add frontend for emoji reactions
this is still pretty bare bones but hey, it works.
This commit is contained in:
parent
ce8b8d9b78
commit
73c6e0a443
5 changed files with 334 additions and 1 deletions
|
@ -41,6 +41,16 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
||||||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||||
|
|
||||||
|
export const STATUS_REACTION_UPDATE = 'STATUS_REACTION_UPDATE';
|
||||||
|
|
||||||
|
export const STATUS_REACTION_ADD_REQUEST = 'STATUS_REACTION_ADD_REQUEST';
|
||||||
|
export const STATUS_REACTION_ADD_SUCCESS = 'STATUS_REACTION_ADD_SUCCESS';
|
||||||
|
export const STATUS_REACTION_ADD_FAIL = 'STATUS_REACTION_ADD_FAIL';
|
||||||
|
|
||||||
|
export const STATUS_REACTION_REMOVE_REQUEST = 'STATUS_REACTION_REMOVE_REQUEST';
|
||||||
|
export const STATUS_REACTION_REMOVE_SUCCESS = 'STATUS_REACTION_REMOVE_SUCCESS';
|
||||||
|
export const STATUS_REACTION_REMOVE_FAIL = 'STATUS_REACTION_REMOVE_FAIL';
|
||||||
|
|
||||||
export function reblog(status, visibility) {
|
export function reblog(status, visibility) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(reblogRequest(status));
|
dispatch(reblogRequest(status));
|
||||||
|
@ -391,4 +401,79 @@ export function unpinFail(status, error) {
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const statusAddReaction = (statusId, name) => (dispatch, getState) => {
|
||||||
|
const status = getState().get('statuses').get(statusId);
|
||||||
|
let alreadyAdded = false;
|
||||||
|
if (status) {
|
||||||
|
const reaction = status.get('reactions').find(x => x.get('name') === name);
|
||||||
|
if (reaction && reaction.get('me')) {
|
||||||
|
alreadyAdded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
dispatch(statusAddReactionRequest(statusId, name, alreadyAdded));
|
||||||
|
}
|
||||||
|
|
||||||
|
api(getState).put(`/api/v1/statuses/${statusId}/reactions/${name}`).then(() => {
|
||||||
|
dispatch(statusAddReactionSuccess(statusId, name, alreadyAdded));
|
||||||
|
}).catch(err => {
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
dispatch(statusAddReactionFail(statusId, name, err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusAddReactionRequest = (statusId, name) => ({
|
||||||
|
type: STATUS_REACTION_ADD_REQUEST,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusAddReactionSuccess = (statusId, name) => ({
|
||||||
|
type: STATUS_REACTION_ADD_SUCCESS,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusAddReactionFail = (statusId, name, error) => ({
|
||||||
|
type: STATUS_REACTION_ADD_FAIL,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusRemoveReaction = (statusId, name) => (dispatch, getState) => {
|
||||||
|
dispatch(statusRemoveReactionRequest(statusId, name));
|
||||||
|
|
||||||
|
api(getState).delete(`/api/v1/statuses/${statusId}/reactions/${name}`).then(() => {
|
||||||
|
dispatch(statusRemoveReactionSuccess(statusId, name));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(statusRemoveReactionFail(statusId, name, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusRemoveReactionRequest = (statusId, name) => ({
|
||||||
|
type: STATUS_REACTION_REMOVE_REQUEST,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusRemoveReactionSuccess = (statusId, name) => ({
|
||||||
|
type: STATUS_REACTION_REMOVE_SUCCESS,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const statusRemoveReactionFail = (statusId, name) => ({
|
||||||
|
type: STATUS_REACTION_REMOVE_FAIL,
|
||||||
|
id: statusId,
|
||||||
|
name,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ import StatusHeader from './status_header';
|
||||||
import StatusIcons from './status_icons';
|
import StatusIcons from './status_icons';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
|
import StatusReactionsBar from './status_reactions_bar';
|
||||||
import AttachmentList from './attachment_list';
|
import AttachmentList from './attachment_list';
|
||||||
import Card from '../features/status/components/card';
|
import Card from '../features/status/components/card';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -74,6 +75,8 @@ class Status extends ImmutablePureComponent {
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: PropTypes.func,
|
onMention: PropTypes.func,
|
||||||
|
onReactionAdd: PropTypes.func,
|
||||||
|
onReactionRemove: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onOpenMedia: PropTypes.func,
|
onOpenMedia: PropTypes.func,
|
||||||
onOpenVideo: PropTypes.func,
|
onOpenVideo: PropTypes.func,
|
||||||
|
@ -101,6 +104,7 @@ class Status extends ImmutablePureComponent {
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
pictureInPicture: ImmutablePropTypes.contains({
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
|
@ -809,6 +813,14 @@ class Status extends ImmutablePureComponent {
|
||||||
rewriteMentions={settings.get('rewrite_mentions')}
|
rewriteMentions={settings.get('rewrite_mentions')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StatusReactionsBar
|
||||||
|
statusId={status.get('id')}
|
||||||
|
reactions={status.get('reactions')}
|
||||||
|
addReaction={this.props.onReactionAdd}
|
||||||
|
removeReaction={this.props.onReactionRemove}
|
||||||
|
emojiMap={this.props.emojiMap}
|
||||||
|
/>
|
||||||
|
|
||||||
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
{!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? (
|
||||||
<StatusActionBar
|
<StatusActionBar
|
||||||
status={status}
|
status={status}
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { reduceMotion } from '../initial_state';
|
||||||
|
import spring from 'react-motion/lib/spring';
|
||||||
|
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import EmojiPickerDropdown from '../features/compose/containers/emoji_picker_dropdown_container';
|
||||||
|
import Icon from './icon';
|
||||||
|
import React from 'react';
|
||||||
|
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
|
||||||
|
import AnimatedNumber from './animated_number';
|
||||||
|
import { assetHost } from '../utils/config';
|
||||||
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
|
export default class StatusReactionsBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEmojiPick = data => {
|
||||||
|
const { addReaction, statusId } = this.props;
|
||||||
|
addReaction(statusId, data.native.replace(/:/g, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
willEnter() {
|
||||||
|
return { scale: reduceMotion ? 1 : 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
willLeave() {
|
||||||
|
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reactions } = this.props;
|
||||||
|
const visibleReactions = reactions.filter(x => x.get('count') > 0);
|
||||||
|
|
||||||
|
const styles = visibleReactions.map(reaction => ({
|
||||||
|
key: reaction.get('name'),
|
||||||
|
data: reaction,
|
||||||
|
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||||
|
})).toArray();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||||
|
{items => (
|
||||||
|
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||||
|
{items.map(({ key, data, style }) => (
|
||||||
|
<Reaction
|
||||||
|
key={key}
|
||||||
|
statusId={this.props.statusId}
|
||||||
|
reaction={data}
|
||||||
|
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||||
|
addReaction={this.props.addReaction}
|
||||||
|
removeReaction={this.props.removeReaction}
|
||||||
|
emojiMap={this.props.emojiMap}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' />} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TransitionMotion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reaction extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string,
|
||||||
|
reaction: ImmutablePropTypes.map.isRequired,
|
||||||
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hovered: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { reaction, statusId, addReaction, removeReaction } = this.props;
|
||||||
|
|
||||||
|
if (reaction.get('me')) {
|
||||||
|
removeReaction(statusId, reaction.get('name'));
|
||||||
|
} else {
|
||||||
|
addReaction(statusId, reaction.get('name'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => this.setState({ hovered: true })
|
||||||
|
|
||||||
|
handleMouseLeave = () => this.setState({ hovered: false })
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { reaction } = this.props;
|
||||||
|
|
||||||
|
let shortCode = reaction.get('name');
|
||||||
|
|
||||||
|
if (unicodeMapping[shortCode]) {
|
||||||
|
shortCode = unicodeMapping[shortCode].shortCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
title={`:${shortCode}:`}
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
|
<span className='reactions-bar__item__emoji'>
|
||||||
|
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
|
||||||
|
</span>
|
||||||
|
<span className='reactions-bar__item__count'>
|
||||||
|
<AnimatedNumber value={reaction.get('count')} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Emoji extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
emoji: PropTypes.string.isRequired,
|
||||||
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
|
hovered: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { emoji, emojiMap, hovered } = this.props;
|
||||||
|
|
||||||
|
if (unicodeMapping[emoji]) {
|
||||||
|
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
||||||
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione'
|
||||||
|
alt={emoji}
|
||||||
|
title={title}
|
||||||
|
src={`${assetHost}/emoji/${filename}.svg`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (emojiMap.get(emoji)) {
|
||||||
|
const filename = (autoPlayGif || hovered)
|
||||||
|
? emojiMap.getIn([emoji, 'url'])
|
||||||
|
: emojiMap.getIn([emoji, 'static_url']);
|
||||||
|
const shortCode = `:${emoji}:`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
draggable='false'
|
||||||
|
className='emojione custom-emoji'
|
||||||
|
alt={shortCode}
|
||||||
|
title={shortCode}
|
||||||
|
src={filename}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,8 @@ import {
|
||||||
unbookmark,
|
unbookmark,
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
|
statusAddReaction,
|
||||||
|
statusRemoveReaction,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import {
|
import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
|
@ -44,6 +46,10 @@ import { showAlertForError } from '../actions/alerts';
|
||||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
import Spoilers from '../components/spoilers';
|
import Spoilers from '../components/spoilers';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -86,6 +92,7 @@ const makeMapStateToProps = () => {
|
||||||
account: account || props.account,
|
account: account || props.account,
|
||||||
settings: state.get('local_settings'),
|
settings: state.get('local_settings'),
|
||||||
prepend: prepend || props.prepend,
|
prepend: prepend || props.prepend,
|
||||||
|
emojiMap: customEmojiMap(state),
|
||||||
pictureInPicture: getPictureInPicture(state, props),
|
pictureInPicture: getPictureInPicture(state, props),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -165,6 +172,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onReactionAdd (statusId, name) {
|
||||||
|
dispatch(statusAddReaction(statusId, name));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReactionRemove (statusId, name) {
|
||||||
|
dispatch(statusRemoveReaction(statusId, name));
|
||||||
|
},
|
||||||
|
|
||||||
onEmbed (status) {
|
onEmbed (status) {
|
||||||
dispatch(openModal('EMBED', {
|
dispatch(openModal('EMBED', {
|
||||||
url: status.get('url'),
|
url: status.get('url'),
|
||||||
|
|
|
@ -6,6 +6,11 @@ import {
|
||||||
UNFAVOURITE_SUCCESS,
|
UNFAVOURITE_SUCCESS,
|
||||||
BOOKMARK_REQUEST,
|
BOOKMARK_REQUEST,
|
||||||
BOOKMARK_FAIL,
|
BOOKMARK_FAIL,
|
||||||
|
STATUS_REACTION_UPDATE,
|
||||||
|
STATUS_REACTION_ADD_FAIL,
|
||||||
|
STATUS_REACTION_REMOVE_FAIL,
|
||||||
|
STATUS_REACTION_ADD_REQUEST,
|
||||||
|
STATUS_REACTION_REMOVE_REQUEST,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import {
|
import {
|
||||||
STATUS_MUTE_SUCCESS,
|
STATUS_MUTE_SUCCESS,
|
||||||
|
@ -37,6 +42,37 @@ const deleteStatus = (state, id, references) => {
|
||||||
return state.delete(id);
|
return state.delete(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateReaction = (state, id, name, updater) => state.update(
|
||||||
|
id,
|
||||||
|
status => status.update(
|
||||||
|
'reactions',
|
||||||
|
reactions => {
|
||||||
|
const index = reactions.findIndex(reaction => reaction.get('name') === name);
|
||||||
|
if (index > -1) {
|
||||||
|
return reactions.update(index, reaction => updater(reaction));
|
||||||
|
} else {
|
||||||
|
return reactions.push(updater(fromJS({ name, count: 0 })));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateReactionCount = (state, reaction) => updateReaction(state, reaction.status_id, reaction.name, x => x.set('count', reaction.count));
|
||||||
|
|
||||||
|
const addReaction = (state, id, name) => updateReaction(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
x => x.set('me', true).update('count', n => n + 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeReaction = (state, id, name) => updateReaction(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
x => x.set('me', false).update('count', n => n - 1),
|
||||||
|
);
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
export default function statuses(state = initialState, action) {
|
||||||
|
@ -63,6 +99,14 @@ export default function statuses(state = initialState, action) {
|
||||||
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
return state.setIn([action.status.get('id'), 'reblogged'], true);
|
||||||
case REBLOG_FAIL:
|
case REBLOG_FAIL:
|
||||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
|
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false);
|
||||||
|
case STATUS_REACTION_UPDATE:
|
||||||
|
return updateReactionCount(state, action.reaction);
|
||||||
|
case STATUS_REACTION_ADD_REQUEST:
|
||||||
|
case STATUS_REACTION_REMOVE_FAIL:
|
||||||
|
return addReaction(state, action.id, action.name);
|
||||||
|
case STATUS_REACTION_REMOVE_REQUEST:
|
||||||
|
case STATUS_REACTION_ADD_FAIL:
|
||||||
|
return removeReaction(state, action.id, action.name);
|
||||||
case STATUS_MUTE_SUCCESS:
|
case STATUS_MUTE_SUCCESS:
|
||||||
return state.setIn([action.id, 'muted'], true);
|
return state.setIn([action.id, 'muted'], true);
|
||||||
case STATUS_UNMUTE_SUCCESS:
|
case STATUS_UNMUTE_SUCCESS:
|
||||||
|
|
Loading…
Reference in a new issue