Add multiple emoji reaction support
This commit is contained in:
parent
c12cefc619
commit
8a31e765cb
36 changed files with 289 additions and 304 deletions
|
@ -18,8 +18,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
#UnEmojiReactionWorker.perform_async(current_account.id, @status.id)
|
||||
if UnEmojiReactionService.new.call(current_account, @status).present?
|
||||
if UnEmojiReactionService.new.call(current_account, @status, params[:id], shortcode_only: true).present?
|
||||
@status = Status.include_expired.find(params[:status_id])
|
||||
end
|
||||
|
||||
|
|
|
@ -877,20 +877,20 @@ export function emojiReactionFail(status, name, domain, url, static_url, error)
|
|||
};
|
||||
};
|
||||
|
||||
const findMyEmojiReaction = (status) => {
|
||||
return status.get('emoji_reactioned') && status.get('emoji_reactions').find(emoji_reaction => emoji_reaction.get('account_ids').includes(me));
|
||||
const findMyEmojiReaction = (status, name) => {
|
||||
return status.get('emoji_reactions').find(emoji_reaction => emoji_reaction.get('account_ids').includes(me) && emoji_reaction.get('name') === name);
|
||||
};
|
||||
|
||||
export function removeEmojiReaction(status) {
|
||||
export function removeEmojiReaction(status, name) {
|
||||
return function (dispatch, getState) {
|
||||
const emoji_reaction = findMyEmojiReaction(status);
|
||||
const emoji_reaction = findMyEmojiReaction(status, name);
|
||||
|
||||
if (emoji_reaction) {
|
||||
const {name, domain, url, static_url} = emoji_reaction.toObject();
|
||||
const { name, domain, url, static_url } = emoji_reaction.toObject();
|
||||
|
||||
dispatch(unEmojiReactionRequest(status, name, domain, url, static_url));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`).then(function (response) {
|
||||
api(getState).delete(`/api/v1/statuses/${status.get('id')}/emoji_reactions/${name}${domain ? `@${domain}` : ''}`).then(function (response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unEmojiReactionSuccess(status, name, domain, url, static_url));
|
||||
}).catch(function (error) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
||||
|
@ -10,7 +9,6 @@ export default class Emoji extends React.PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
emoji: PropTypes.string.isRequired,
|
||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||
className: PropTypes.string,
|
||||
hovered: PropTypes.bool.isRequired,
|
||||
url: PropTypes.string,
|
||||
|
@ -18,7 +16,7 @@ export default class Emoji extends React.PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { emoji, emojiMap, hovered, url, static_url } = this.props;
|
||||
const { emoji, hovered, url, static_url } = this.props;
|
||||
|
||||
if (unicodeMapping[emoji]) {
|
||||
const { filename, shortCode } = unicodeMapping[emoji];
|
||||
|
@ -34,20 +32,6 @@ export default class Emoji extends React.PureComponent {
|
|||
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}:`;
|
||||
const className = classNames('emojione custom-emoji', this.props.className);
|
||||
|
||||
return (
|
||||
<img
|
||||
draggable='false'
|
||||
className={className}
|
||||
alt={shortCode}
|
||||
title={shortCode}
|
||||
src={filename}
|
||||
/>
|
||||
);
|
||||
} else if (url || static_url) {
|
||||
const filename = (autoPlayGif || hovered) && url ? url : static_url;
|
||||
const shortCode = `:${emoji}:`;
|
||||
|
|
137
app/javascript/mastodon/components/emoji_reactions.js
Normal file
137
app/javascript/mastodon/components/emoji_reactions.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List } from 'immutable';
|
||||
import classNames from 'classnames';
|
||||
import Emoji from './emoji';
|
||||
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||
import { disableReactions } from 'mastodon/initial_state';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { isUserTouching } from 'mastodon/is_mobile';
|
||||
import AccountPopup from 'mastodon/components/account_popup';
|
||||
|
||||
const getFilteredEmojiReaction = (emojiReaction, relationships) => {
|
||||
let filteredEmojiReaction = emojiReaction.update('account_ids', accountIds => accountIds.filterNot( accountId => {
|
||||
const relationship = relationships.get(accountId);
|
||||
return relationship?.get('blocking') || relationship?.get('blocked_by') || relationship?.get('domain_blocking') || relationship?.get('muting')
|
||||
}));
|
||||
|
||||
const count = filteredEmojiReaction.get('account_ids').size;
|
||||
|
||||
if (count > 0) {
|
||||
return filteredEmojiReaction.set('count', count);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { emojiReaction }) => {
|
||||
const relationship = new Map();
|
||||
emojiReaction.get('account_ids').forEach(accountId => relationship.set(accountId, state.getIn(['relationships', accountId])));
|
||||
|
||||
return {
|
||||
emojiReaction: emojiReaction,
|
||||
relationships: relationship,
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = ({ emojiReaction, relationships }, dispatchProps, ownProps) => ({
|
||||
...ownProps,
|
||||
...dispatchProps,
|
||||
emojiReaction: getFilteredEmojiReaction(emojiReaction, relationships),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, null, mergeProps)
|
||||
export default class EmojiReaction extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
emojiReaction: ImmutablePropTypes.map,
|
||||
myReaction: PropTypes.bool.isRequired,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
hovered: false,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { emojiReaction, status, addEmojiReaction, removeEmojiReaction, myReaction } = this.props;
|
||||
|
||||
if (myReaction) {
|
||||
removeEmojiReaction(status, emojiReaction.get('name'));
|
||||
} else {
|
||||
addEmojiReaction(status, emojiReaction.get('name'), emojiReaction.get('domain', null), emojiReaction.get('url', null), emojiReaction.get('static_url', null));
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseEnter = ({ target }) => {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
||||
this.setState({
|
||||
hovered: true,
|
||||
placement: top * 2 < innerHeight ? 'bottom' : 'top',
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({
|
||||
hovered: false,
|
||||
});
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.target?.addEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.addEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.target?.removeEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.removeEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { style, emojiReaction, myReaction, reactionLimitReached } = this.props;
|
||||
|
||||
if (!emojiReaction) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
let shortCode = emojiReaction.get('name');
|
||||
|
||||
if (unicodeMapping[shortCode]) {
|
||||
shortCode = unicodeMapping[shortCode].shortCode;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='reactions-bar__item-wrapper' ref={this.setTargetRef}>
|
||||
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={disableReactions || !myReaction && reactionLimitReached} onClick={this.handleClick} title={`:${shortCode}:`} style={style}>
|
||||
<span className='reactions-bar__item__emoji'><Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
|
||||
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
|
||||
</button>
|
||||
</div>
|
||||
{!isUserTouching() &&
|
||||
<Overlay show={this.state.hovered} placement={this.state.placement} target={this.findTarget}>
|
||||
<AccountPopup accountIds={emojiReaction.get('account_ids', List())} />
|
||||
</Overlay>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
|
@ -4,147 +4,30 @@ import { connect } from 'react-redux';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List } from 'immutable';
|
||||
import classNames from 'classnames';
|
||||
import Emoji from './emoji';
|
||||
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||
import { reduceMotion, me, disableReactions } from 'mastodon/initial_state';
|
||||
import { reduceMotion, me } from 'mastodon/initial_state';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { isUserTouching } from 'mastodon/is_mobile';
|
||||
import AccountPopup from 'mastodon/components/account_popup';
|
||||
import EmojiReaction from './emoji_reactions';
|
||||
|
||||
const getFilteredEmojiReaction = (emojiReaction, relationships) => {
|
||||
let filteredEmojiReaction = emojiReaction.update('account_ids', accountIds => accountIds.filterNot( accountId => {
|
||||
const relationship = relationships.get(accountId);
|
||||
return relationship?.get('blocking') || relationship?.get('blocked_by') || relationship?.get('domain_blocking') || relationship?.get('muting')
|
||||
}));
|
||||
const mapStateToProps = (state, { status }) => ({
|
||||
emojiReactions: status.get('emoji_reactions'),
|
||||
});
|
||||
|
||||
const count = filteredEmojiReaction.get('account_ids').size;
|
||||
|
||||
if (count > 0) {
|
||||
return filteredEmojiReaction.set('count', count);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { emojiReaction }) => {
|
||||
const relationship = new Map();
|
||||
emojiReaction.get('account_ids').forEach(accountId => relationship.set(accountId, state.getIn(['relationships', accountId])));
|
||||
|
||||
return {
|
||||
emojiReaction: emojiReaction,
|
||||
relationships: relationship,
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = ({ emojiReaction, relationships }, dispatchProps, ownProps) => ({
|
||||
const mergeProps = ({ emojiReactions }, dispatchProps, ownProps) => ({
|
||||
...ownProps,
|
||||
...dispatchProps,
|
||||
emojiReaction: getFilteredEmojiReaction(emojiReaction, relationships),
|
||||
visibleReactions: emojiReactions.filter(x => x.get('count') > 0),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, null, mergeProps)
|
||||
class EmojiReaction extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
emojiReaction: ImmutablePropTypes.map,
|
||||
myReaction: PropTypes.bool.isRequired,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
hovered: false,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { emojiReaction, status, addEmojiReaction, removeEmojiReaction, myReaction } = this.props;
|
||||
|
||||
if (myReaction) {
|
||||
removeEmojiReaction(status);
|
||||
} else {
|
||||
addEmojiReaction(status, emojiReaction.get('name'), emojiReaction.get('domain', null), emojiReaction.get('url', null), emojiReaction.get('static_url', null));
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseEnter = ({ target }) => {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
||||
this.setState({
|
||||
hovered: true,
|
||||
placement: top * 2 < innerHeight ? 'bottom' : 'top',
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({
|
||||
hovered: false,
|
||||
});
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.target?.addEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.addEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.target?.removeEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.removeEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { emojiReaction, status, myReaction } = this.props;
|
||||
|
||||
if (!emojiReaction) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
let shortCode = emojiReaction.get('name');
|
||||
|
||||
if (unicodeMapping[shortCode]) {
|
||||
shortCode = unicodeMapping[shortCode].shortCode;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='reactions-bar__item-wrapper' ref={this.setTargetRef}>
|
||||
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={disableReactions || status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} title={`:${shortCode}:`} style={this.props.style}>
|
||||
<span className='reactions-bar__item__emoji'><Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
|
||||
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
|
||||
</button>
|
||||
</div>
|
||||
{!isUserTouching() &&
|
||||
<Overlay show={this.state.hovered} placement={this.state.placement} target={this.findTarget}>
|
||||
<AccountPopup accountIds={emojiReaction.get('account_ids', List())} />
|
||||
</Overlay>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default class EmojiReactionsBar extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||
visibleReactions: ImmutablePropTypes.list.isRequired,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
willEnter () {
|
||||
|
@ -156,22 +39,20 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status } = this.props;
|
||||
const emoji_reactions = status.get('emoji_reactions');
|
||||
const visibleReactions = emoji_reactions.filter(x => x.get('count') > 0);
|
||||
const { status, addEmojiReaction, removeEmojiReaction, visibleReactions, reactionLimitReached } = this.props;
|
||||
|
||||
if (visibleReactions.isEmpty() ) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
const styles = visibleReactions.map(emoji_reaction => {
|
||||
const domain = emoji_reaction.get('domain', '');
|
||||
const styles = visibleReactions.map(emojiReaction => {
|
||||
const domain = emojiReaction.get('domain', '');
|
||||
|
||||
return {
|
||||
key: `${emoji_reaction.get('name')}${domain ? `@${domain}` : ''}`,
|
||||
key: `${emojiReaction.get('name')}${domain ? `@${domain}` : ''}`,
|
||||
data: {
|
||||
emojiReaction: emoji_reaction,
|
||||
myReaction: emoji_reaction.get('account_ids', List()).includes(me),
|
||||
emojiReaction: emojiReaction,
|
||||
myReaction: emojiReaction.get('account_ids', List()).includes(me),
|
||||
},
|
||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||
};
|
||||
|
@ -187,10 +68,10 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
|
|||
emojiReaction={data.emojiReaction}
|
||||
myReaction={data.myReaction}
|
||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||
status={this.props.status}
|
||||
addEmojiReaction={this.props.addEmojiReaction}
|
||||
removeEmojiReaction={this.props.removeEmojiReaction}
|
||||
emojiMap={this.props.emojiMap}
|
||||
status={status}
|
||||
addEmojiReaction={addEmojiReaction}
|
||||
removeEmojiReaction={removeEmojiReaction}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -286,7 +286,6 @@ export default class ReactionPickerDropdown extends React.PureComponent {
|
|||
openedViaKeyboard: PropTypes.bool,
|
||||
custom_emojis: ImmutablePropTypes.list,
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
onRemoveEmoji: PropTypes.func.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
|
@ -305,9 +304,7 @@ export default class ReactionPickerDropdown extends React.PureComponent {
|
|||
};
|
||||
|
||||
handleClick = ({ target, type }) => {
|
||||
if (this.props.pressed) {
|
||||
this.props.onRemoveEmoji();
|
||||
} else if (this.state.id === this.props.openDropdownId) {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
|
|
@ -165,9 +165,9 @@ class Status extends ImmutablePureComponent {
|
|||
available: PropTypes.bool,
|
||||
}),
|
||||
contextType: PropTypes.string,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -404,7 +404,7 @@ class Status extends ImmutablePureComponent {
|
|||
let media = null;
|
||||
let statusAvatar, prepend, rebloggedByText;
|
||||
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, showCard, scrollKey, pictureInPicture, contextType, quote_muted, referenced, contextReferenced } = this.props;
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, showCard, scrollKey, pictureInPicture, contextType, quote_muted, referenced, contextReferenced, reactionLimitReached } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
|
@ -796,7 +796,7 @@ class Status extends ImmutablePureComponent {
|
|||
status={status}
|
||||
addEmojiReaction={this.props.addEmojiReaction}
|
||||
removeEmojiReaction={this.props.removeEmojiReaction}
|
||||
emojiMap={this.props.emojiMap}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
/>}
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} expired={expired} {...other} />
|
||||
</div>
|
||||
|
|
|
@ -111,6 +111,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
addEmojiReaction: PropTypes.func,
|
||||
removeEmojiReaction: PropTypes.func,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -327,7 +328,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, relationship, intl, withDismiss, scrollKey, expired, referenced, contextReferenced, referenceCountLimit, contextType } = this.props;
|
||||
const { status, relationship, intl, withDismiss, scrollKey, expired, referenced, contextReferenced, referenceCountLimit, contextType, reactionLimitReached } = this.props;
|
||||
|
||||
const anonymousAccess = !me;
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
|
@ -339,7 +340,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
const reblogged = status.get('reblogged');
|
||||
const favourited = status.get('favourited');
|
||||
const bookmarked = status.get('bookmarked');
|
||||
const emoji_reactioned = status.get('emoji_reactioned');
|
||||
const reblogsCount = status.get('reblogs_count');
|
||||
const referredByCount = status.get('status_referred_by_count');
|
||||
const favouritesCount = status.get('favourites_count');
|
||||
|
@ -480,7 +480,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' disabled={disablePost || expired} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
{enableStatusReference && me && <IconButton className={classNames('status__action-bar-button link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} />}
|
||||
{enableStatusReference && me && <IconButton className={classNames('status__action-bar-button link-icon', { referenced, 'context-referenced': contextReferenced })} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} />}
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={disableReactions || !publicStatus && !reblogPrivate || expired} active={reblogged} pressed={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate disabled={disableReactions || !favourited && expired} active={favourited} pressed={favourited} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||
{show_quote_button && <IconButton className='status__action-bar-button' disabled={disablePost || anonymousAccess || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />}
|
||||
|
@ -490,9 +490,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
{enableReaction && <div className={classNames('status__action-bar-dropdown', { 'icon-button--with-counter': reactionsCounter })}>
|
||||
<ReactionPickerDropdownContainer
|
||||
scrollKey={scrollKey}
|
||||
disabled={disableReactions || expired || anonymousAccess}
|
||||
active={emoji_reactioned}
|
||||
pressed={emoji_reactioned}
|
||||
disabled={disableReactions || expired || anonymousAccess || reactionLimitReached}
|
||||
className='status__action-bar-button'
|
||||
status={status}
|
||||
title={intl.formatMessage(messages.emoji_reaction)}
|
||||
|
@ -502,6 +500,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
counter={reactionsCounter}
|
||||
onPickEmoji={this.handleEmojiPick}
|
||||
onRemoveEmoji={this.handleEmojiRemove}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ class StatusItem extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map,
|
||||
onClick: PropTypes.func,
|
||||
onUnselectReference: PropTypes.func,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
updateOnProps = [
|
||||
|
|
|
@ -65,10 +65,6 @@ const getCustomEmojis = createSelector([
|
|||
}
|
||||
}));
|
||||
|
||||
const getState = (dispatch) => new Promise((resolve) => {
|
||||
dispatch((dispatch, getState) => {resolve(getState())})
|
||||
})
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
custom_emojis: getCustomEmojis(state),
|
||||
skinTone: state.getIn(['settings', 'skinTone']),
|
||||
|
|
|
@ -52,9 +52,8 @@ import { deployPictureInPicture } from '../actions/picture_in_picture';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { boostModal, deleteModal, unfollowModal, unsubscribeModal, confirmDomainBlock } from '../initial_state';
|
||||
import { showAlertForError } from '../actions/alerts';
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { me, maxReactionsPerAccount } from 'mastodon/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -74,7 +73,6 @@ const messages = defineMessages({
|
|||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
const getProper = (status) => status.get('reblog', null) !== null && typeof status.get('reblog') === 'object' ? status.get('reblog') : status;
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
|
@ -84,16 +82,26 @@ const makeMapStateToProps = () => {
|
|||
return {
|
||||
status,
|
||||
pictureInPicture: getPictureInPicture(state, props),
|
||||
emojiMap: customEmojiMap(state),
|
||||
id,
|
||||
referenced: state.getIn(['compose', 'references']).has(id),
|
||||
contextReferenced: state.getIn(['compose', 'context_references']).has(id),
|
||||
emojiReactions: !!status ? status.get('emoji_reactions', ImmutableList()) : ImmutableList(),
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mergeProps = ({ status, pictureInPicture, referenced, contextReferenced, emojiReactions }, dispatchProps, ownProps) => ({
|
||||
...ownProps,
|
||||
...dispatchProps,
|
||||
status,
|
||||
pictureInPicture,
|
||||
referenced,
|
||||
contextReferenced,
|
||||
reactionLimitReached: emojiReactions.count((emojiReaction) => emojiReaction.get('account_ids', ImmutableList()).includes(me)) >= maxReactionsPerAccount,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onReply (status, router) {
|
||||
|
@ -308,8 +316,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(addEmojiReaction(status, name, domain, url, static_url));
|
||||
},
|
||||
|
||||
removeEmojiReaction (status) {
|
||||
dispatch(removeEmojiReaction(status));
|
||||
removeEmojiReaction (status, name) {
|
||||
dispatch(removeEmojiReaction(status, name));
|
||||
},
|
||||
|
||||
onAddReference (id, change) {
|
||||
|
@ -322,4 +330,4 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps, mergeProps)(Status));
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import StatusItem from '../components/status_item';
|
||||
import { makeGetStatus } from '../selectors';
|
||||
import {
|
||||
removeReference,
|
||||
} from '../actions/compose';
|
||||
import { removeReference } from '../actions/compose';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { unselectReferenceModal } from '../initial_state';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
unselectMessage: { id: 'confirmations.unselect.message', defaultMessage: 'Are you sure you want to unselect a reference?' },
|
||||
unselectConfirm: { id: 'confirmations.unselect.confirm', defaultMessage: 'Unselect' },
|
||||
|
@ -19,16 +14,10 @@ const messages = defineMessages({
|
|||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const status = getStatus(state, props);
|
||||
|
||||
return {
|
||||
status,
|
||||
emojiMap: customEmojiMap(state),
|
||||
}
|
||||
};
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
|
|
@ -12,8 +12,6 @@ import ScrollableList from '../../components/scrollable_list';
|
|||
import Icon from 'mastodon/components/icon';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import Emoji from '../../components/emoji';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import ReactedHeaderContaier from '../reactioned/containers/header_container';
|
||||
import { debounce } from 'lodash';
|
||||
import { defaultColumnWidth } from 'mastodon/initial_state';
|
||||
|
@ -24,8 +22,6 @@ const messages = defineMessages({
|
|||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
});
|
||||
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state, { columnId, params }) => {
|
||||
const uuid = columnId;
|
||||
const columns = state.getIn(['settings', 'columns']);
|
||||
|
@ -36,7 +32,6 @@ const mapStateToProps = (state, { columnId, params }) => {
|
|||
emojiReactions: state.getIn(['user_lists', 'emoji_reactioned_by', params.statusId, 'items']),
|
||||
isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', params.statusId, 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', params.statusId, 'next']),
|
||||
emojiMap: customEmojiMap(state),
|
||||
columnWidth: columnWidth ?? defaultColumnWidth,
|
||||
};
|
||||
};
|
||||
|
@ -45,7 +40,6 @@ class Reaction extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
emojiReaction: ImmutablePropTypes.map.isRequired,
|
||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -57,11 +51,11 @@ class Reaction extends ImmutablePureComponent {
|
|||
handleMouseLeave = () => this.setState({ hovered: false })
|
||||
|
||||
render () {
|
||||
const { emojiReaction, emojiMap } = this.props;
|
||||
const { emojiReaction } = this.props;
|
||||
|
||||
return (
|
||||
<div className='account__emoji_reaction' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} />
|
||||
<Emoji className='reaction' hovered={this.state.hovered} emoji={emojiReaction.get('name')} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -77,7 +71,6 @@ class EmojiReactions extends ImmutablePureComponent {
|
|||
emojiReactions: ImmutablePropTypes.list,
|
||||
multiColumn: PropTypes.bool,
|
||||
columnWidth: PropTypes.string,
|
||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -114,7 +107,7 @@ class EmojiReactions extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl, emojiReactions, multiColumn, emojiMap, hasMore, isLoading, columnWidth } = this.props;
|
||||
const { intl, emojiReactions, multiColumn, hasMore, isLoading, columnWidth } = this.props;
|
||||
|
||||
if (!emojiReactions) {
|
||||
return (
|
||||
|
@ -149,7 +142,7 @@ class EmojiReactions extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{emojiReactions.map(emojiReaction =>
|
||||
<AccountContainer key={emojiReaction.get('account')+emojiReaction.get('name')} id={emojiReaction.get('account')} withNote={false} append={<Reaction emojiReaction={emojiReaction} emojiMap={emojiMap} />} />,
|
||||
<AccountContainer key={emojiReaction.get('account')+emojiReaction.get('name')} id={emojiReaction.get('account')} withNote={false} append={<Reaction emojiReaction={emojiReaction} />} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
|
|
@ -56,7 +56,6 @@ class Notification extends ImmutablePureComponent {
|
|||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
unread: PropTypes.bool,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
handleMoveUp = () => {
|
||||
|
@ -349,7 +348,7 @@ class Notification extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
renderReaction (notification, link) {
|
||||
const { intl, unread, emojiMap } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
if (!notification.get('emoji_reaction')) {
|
||||
return <Fragment />;
|
||||
|
@ -362,7 +361,7 @@ class Notification extends ImmutablePureComponent {
|
|||
<div className={classNames('notification notification-reaction focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.emoji_reaction, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className={classNames('notification__reaction-icon-wrapper', { wide })}>
|
||||
<Emoji hovered={false} emoji={notification.getIn(['emoji_reaction', 'name'])} emojiMap={emojiMap} url={notification.getIn(['emoji_reaction', 'url'])} static_url={notification.getIn(['emoji_reaction', 'static_url'])} />
|
||||
<Emoji hovered={false} emoji={notification.getIn(['emoji_reaction', 'name'])} url={notification.getIn(['emoji_reaction', 'url'])} static_url={notification.getIn(['emoji_reaction', 'static_url'])} />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')} className={classNames('notification__reaction-message-wrapper', { wide })}>
|
||||
|
|
|
@ -14,20 +14,16 @@ import {
|
|||
revealStatus,
|
||||
} from '../../../actions/statuses';
|
||||
import { boostModal } from '../../../initial_state';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNotification = makeGetNotification();
|
||||
const getStatus = makeGetStatus();
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const notification = getNotification(state, props.notification, props.accountId);
|
||||
return {
|
||||
notification: notification,
|
||||
status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
|
||||
emojiMap: customEmojiMap(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ class ScheduledStatus extends ImmutablePureComponent {
|
|||
onMoveDown: PropTypes.func,
|
||||
onDeleteScheduledStatus: PropTypes.func,
|
||||
onRedraftScheduledStatus: PropTypes.func,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
|
|
|
@ -4,8 +4,6 @@ import { deleteScheduledStatus, redraftScheduledStatus } from 'mastodon/actions/
|
|||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { deleteScheduledStatusModal } from 'mastodon/initial_state';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -14,16 +12,6 @@ const messages = defineMessages({
|
|||
redraftMessage: { id: 'confirmations.redraft_scheduled_status.message', defaultMessage: 'Redraft now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
emojiMap: customEmojiMap(state),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onDeleteScheduledStatus (id, e) {
|
||||
|
@ -56,4 +44,4 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(ScheduledStatus));
|
||||
export default injectIntl(connect(null, mapDispatchToProps)(ScheduledStatus));
|
||||
|
|
|
@ -72,6 +72,7 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
referenced: PropTypes.bool,
|
||||
contextReferenced: PropTypes.bool,
|
||||
relationship: ImmutablePropTypes.map,
|
||||
|
@ -102,6 +103,7 @@ class ActionBar extends React.PureComponent {
|
|||
intl: PropTypes.object.isRequired,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
|
@ -281,7 +283,7 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, relationship, intl, referenced, contextReferenced, referenceCountLimit } = this.props;
|
||||
const { status, relationship, intl, referenced, contextReferenced, referenceCountLimit, reactionLimitReached } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||
|
@ -292,7 +294,6 @@ class ActionBar extends React.PureComponent {
|
|||
const reblogged = status.get('reblogged');
|
||||
const favourited = status.get('favourited');
|
||||
const bookmarked = status.get('bookmarked');
|
||||
const emoji_reactioned = status.get('emoji_reactioned');
|
||||
const reblogsCount = status.get('reblogs_count');
|
||||
const referredByCount = status.get('status_referred_by_count');
|
||||
const favouritesCount = status.get('favourites_count');
|
||||
|
@ -424,7 +425,7 @@ class ActionBar extends React.PureComponent {
|
|||
return (
|
||||
<div className='detailed-status__action-bar'>
|
||||
<div className='detailed-status__button'><IconButton disabled={disablePost || expired} title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
||||
{enableStatusReference && me && <div className='detailed-status__button'><IconButton className={classNames('link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} /></div>}
|
||||
{enableStatusReference && me && <div className='detailed-status__button'><IconButton className={classNames('link-icon', { referenced, 'context-referenced': contextReferenced })} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} /></div>}
|
||||
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={disableReactions || !publicStatus && !reblogPrivate || expired} active={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={favourited} disabled={disableReactions || !favourited && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||
{show_quote_button && <div className='detailed-status__button'><IconButton disabled={disablePost || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>}
|
||||
|
@ -433,9 +434,7 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
{enableReaction && <div className='detailed-status__action-bar-dropdown'>
|
||||
<ReactionPickerDropdownContainer
|
||||
disabled={disableReactions || expired}
|
||||
active={emoji_reactioned}
|
||||
pressed={emoji_reactioned}
|
||||
disabled={disableReactions || expired || reactionLimitReached}
|
||||
className='status__action-bar-button'
|
||||
status={status}
|
||||
title={intl.formatMessage(messages.emoji_reaction)}
|
||||
|
@ -444,6 +443,7 @@ class ActionBar extends React.PureComponent {
|
|||
direction='right'
|
||||
onPickEmoji={this.handleEmojiPick}
|
||||
onRemoveEmoji={this.handleEmojiRemove}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
|
|
|
@ -93,10 +93,10 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
onToggleMediaVisibility: PropTypes.func,
|
||||
showQuoteMedia: PropTypes.bool,
|
||||
onToggleQuoteMediaVisibility: PropTypes.func,
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
addEmojiReaction: PropTypes.func.isRequired,
|
||||
removeEmojiReaction: PropTypes.func.isRequired,
|
||||
onReference: PropTypes.func,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -181,7 +181,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const quote_muted = this.props.quote_muted;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
const { intl, compact, pictureInPicture, referenced, contextReferenced } = this.props;
|
||||
const { intl, compact, pictureInPicture, referenced, contextReferenced, reactionLimitReached } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
|
@ -467,7 +467,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
status={status}
|
||||
addEmojiReaction={this.props.addEmojiReaction}
|
||||
removeEmojiReaction={this.props.removeEmojiReaction}
|
||||
emojiMap={this.props.emojiMap}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
/>}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
|
|
|
@ -32,9 +32,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import { boostModal, deleteModal } from '../../../initial_state';
|
||||
import { showAlertForError } from '../../../actions/alerts';
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
|
@ -47,13 +44,11 @@ const messages = defineMessages({
|
|||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
pictureInPicture: getPictureInPicture(state, props),
|
||||
emojiMap: customEmojiMap(state),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -184,8 +179,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(addEmojiReaction(status, name, domain, url, static_url));
|
||||
},
|
||||
|
||||
removeEmojiReaction (status) {
|
||||
dispatch(removeEmojiReaction(status));
|
||||
removeEmojiReaction (status, name) {
|
||||
dispatch(removeEmojiReaction(status, name));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
|
@ -63,7 +63,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
|
|||
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import DetailedHeaderContaier from './containers/header_container';
|
||||
import { defaultColumnWidth } from 'mastodon/initial_state';
|
||||
import { defaultColumnWidth, me, maxReactionsPerAccount } from 'mastodon/initial_state';
|
||||
import { changeSetting } from '../../actions/settings';
|
||||
import { changeColumnParams } from '../../actions/columns';
|
||||
|
||||
|
@ -86,7 +86,6 @@ const messages = defineMessages({
|
|||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
||||
const getProper = (status) => status.get('reblog', null) !== null && typeof status.get('reblog') === 'object' ? status.get('reblog') : status;
|
||||
|
||||
const getAncestorsIds = createSelector([
|
||||
|
@ -160,6 +159,7 @@ const makeMapStateToProps = () => {
|
|||
const descendantsIds = status ? getDescendantsIds(state, { id: status.get('id') }) : ImmutableList();
|
||||
const referencesIds = status ? getReferencesIds(state, { id: status.get('id') }) : ImmutableList();
|
||||
const id = status ? getProper(status).get('id') : null;
|
||||
const emojiReactions = status ? status.get('emoji_reactions', ImmutableList()) : ImmutableList();
|
||||
|
||||
return {
|
||||
status,
|
||||
|
@ -168,10 +168,10 @@ const makeMapStateToProps = () => {
|
|||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
pictureInPicture: getPictureInPicture(state, { id: params.statusId }),
|
||||
emojiMap: customEmojiMap(state),
|
||||
referenced: state.getIn(['compose', 'references']).has(id),
|
||||
contextReferenced: state.getIn(['compose', 'context_references']).has(id),
|
||||
columnWidth: columnWidth ?? defaultColumnWidth,
|
||||
reactionLimitReached: emojiReactions.count((emojiReaction) => emojiReaction.get('account_ids', ImmutableList()).includes(me)) >= maxReactionsPerAccount,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -203,7 +203,7 @@ class Status extends ImmutablePureComponent {
|
|||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
}),
|
||||
emojiMap: ImmutablePropTypes.map,
|
||||
reactionLimitReached: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -473,8 +473,8 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.dispatch(addEmojiReaction(status, name, domain, url, static_url));
|
||||
}
|
||||
|
||||
handleRemoveEmojiReaction = (status) => {
|
||||
this.props.dispatch(removeEmojiReaction(status));
|
||||
handleRemoveEmojiReaction = (status, name) => {
|
||||
this.props.dispatch(removeEmojiReaction(status, name));
|
||||
}
|
||||
|
||||
handleAddReference = (id, change) => {
|
||||
|
@ -575,7 +575,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture, emojiMap, referenced, contextReferenced, columnWidth } = this.props;
|
||||
const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture, referenced, contextReferenced, columnWidth, reactionLimitReached } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
|
||||
if (status === null) {
|
||||
|
@ -652,7 +652,6 @@ class Status extends ImmutablePureComponent {
|
|||
pictureInPicture={pictureInPicture}
|
||||
showQuoteMedia={this.state.showQuoteMedia}
|
||||
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
|
||||
emojiMap={emojiMap}
|
||||
addEmojiReaction={this.handleAddEmojiReaction}
|
||||
removeEmojiReaction={this.handleRemoveEmojiReaction}
|
||||
/>
|
||||
|
@ -662,6 +661,7 @@ class Status extends ImmutablePureComponent {
|
|||
status={status}
|
||||
referenced={referenced}
|
||||
contextReferenced={contextReferenced}
|
||||
reactionLimitReached={reactionLimitReached}
|
||||
onReply={this.handleReplyClick}
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
|
|
|
@ -79,6 +79,7 @@ export const hideLinkPreview = getMeta('hide_link_preview');
|
|||
export const hidePhotoPreview = getMeta('hide_photo_preview');
|
||||
export const hideVideoPreview = getMeta('hide_video_preview');
|
||||
export const allowPollImage = getMeta('allow_poll_image');
|
||||
export const maxReactionsPerAccount = initialState?.emoji_reactions?.max_reactions_per_account ?? 1;
|
||||
|
||||
export const maxChars = initialState?.max_toot_chars ?? 500;
|
||||
|
||||
|
|
|
@ -87,14 +87,12 @@ export default function statuses(state = initialState, action) {
|
|||
case EMOJI_REACTION_REQUEST:
|
||||
case UN_EMOJI_REACTION_FAIL:
|
||||
if (state.get(action.status.get('id')) !== undefined) {
|
||||
state = state.setIn([action.status.get('id'), 'emoji_reactioned'], true);
|
||||
state = addEmojiReaction(state, action.status.get('id'), action.name, action.domain, action.url, action.static_url);
|
||||
}
|
||||
return state;
|
||||
case UN_EMOJI_REACTION_REQUEST:
|
||||
case EMOJI_REACTION_FAIL:
|
||||
if (state.get(action.status.get('id')) !== undefined) {
|
||||
state = state.setIn([action.status.get('id'), 'emoji_reactioned'], false);
|
||||
state = removeEmojiReaction(state, action.status.get('id'), action.name, action.domain, action.url, action.static_url);
|
||||
}
|
||||
return state;
|
||||
|
|
|
@ -40,16 +40,17 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
return if @account.reacted?(@original_status, shortcode, emoji)
|
||||
reaction = EmojiReaction.find_or_create_by(account: @account, status: @original_status, name: shortcode, custom_emoji: emoji, uri: @json['id'])
|
||||
|
||||
EmojiReaction.find_by(account: @account, status: @original_status)&.destroy!
|
||||
reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id'])
|
||||
return unless reaction
|
||||
|
||||
reaction.tap do |reaction|
|
||||
if @original_status.account.local?
|
||||
NotifyService.new.call(@original_status.account, :emoji_reaction, reaction)
|
||||
forward_for_emoji_reaction
|
||||
relay_for_emoji_reaction
|
||||
end
|
||||
end
|
||||
rescue Seahorse::Client::NetworkingError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -37,6 +37,7 @@ class Form::AdminSettings
|
|||
require_invite_text
|
||||
allow_poll_image
|
||||
poll_max_options
|
||||
reaction_max_per_account
|
||||
).freeze
|
||||
|
||||
BOOLEAN_KEYS = %i(
|
||||
|
@ -58,6 +59,7 @@ class Form::AdminSettings
|
|||
|
||||
INTEGER_KEYS = %i(
|
||||
poll_max_options
|
||||
reaction_max_per_account
|
||||
).freeze
|
||||
|
||||
UPLOAD_KEYS = %i(
|
||||
|
@ -78,6 +80,7 @@ class Form::AdminSettings
|
|||
validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }
|
||||
validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }
|
||||
validates :poll_max_options, numericality: { greater_than: 2, less_than_or_equal_to: PollValidator::MAX_OPTIONS_LIMIT }
|
||||
validates :reaction_max_per_account, numericality: { greater_than_or_equal: 1, less_than_or_equal_to: EmojiReactionValidator::MAX_PER_ACCOUNT_LIMIT }
|
||||
|
||||
def initialize(_attributes = {})
|
||||
super
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
class InitialStateSerializer < ActiveModel::Serializer
|
||||
attributes :meta, :compose, :search, :accounts, :lists,
|
||||
:media_attachments, :status_references, :settings, :max_toot_chars
|
||||
:media_attachments, :status_references, :emoji_reactions,
|
||||
:settings, :max_toot_chars
|
||||
|
||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
|
||||
|
@ -162,6 +163,10 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
{ max_references: StatusReferenceValidator::LIMIT }
|
||||
end
|
||||
|
||||
def emoji_reactions
|
||||
{ max_reactions_per_account: [EmojiReactionValidator::MAX_PER_ACCOUNT, Setting.reaction_max_per_account].max }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instance_presenter
|
||||
|
|
|
@ -94,6 +94,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
|
||||
emoji_reactions: {
|
||||
max_reactions: EmojiReactionValidator::LIMIT,
|
||||
max_reactions_per_account: [EmojiReactionValidator::MAX_PER_ACCOUNT, Setting.reaction_max_per_account].max,
|
||||
},
|
||||
|
||||
status_references: {
|
||||
|
|
|
@ -6,21 +6,17 @@ class EmojiReactionService < BaseService
|
|||
|
||||
def call(account, status, emoji)
|
||||
@account = account
|
||||
|
||||
emoji_reaction = EmojiReaction.find_by(account_id: account.id, status_id: status.id)
|
||||
|
||||
return emoji_reaction unless emoji_reaction.nil?
|
||||
|
||||
shortcode, domain = emoji.split("@")
|
||||
|
||||
return if account.nil? || status.nil? || shortcode.nil?
|
||||
|
||||
custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
|
||||
emoji_reaction = EmojiReaction.find_or_create_by!(account_id: account.id, status_id: status.id, name: shortcode, custom_emoji: custom_emoji)
|
||||
|
||||
emoji_reaction = EmojiReaction.create!(account: account, status: status, name: shortcode, custom_emoji: custom_emoji)
|
||||
|
||||
emoji_reaction.tap do |emoji_reaction|
|
||||
create_notification(emoji_reaction)
|
||||
bump_potential_friendship(account, status)
|
||||
|
||||
emoji_reaction
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -3,14 +3,25 @@
|
|||
class UnEmojiReactionService < BaseService
|
||||
include Payloadable
|
||||
|
||||
def call(account, status)
|
||||
def call(account, status, emoji, **options)
|
||||
@account = account
|
||||
shortcode, domain = emoji&.split("@")
|
||||
|
||||
emoji_reaction = EmojiReaction.find_by!(account: account, status: status)
|
||||
if shortcode
|
||||
if options[:shortcode_only]
|
||||
emoji_reactions = EmojiReaction.where(account: account, status: status, name: shortcode)
|
||||
else
|
||||
custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
|
||||
emoji_reactions = EmojiReaction.where(account: account, status: status, name: shortcode, custom_emoji: custom_emoji)
|
||||
end
|
||||
else
|
||||
emoji_reactions = EmojiReaction.where(account: account, status: status)
|
||||
end
|
||||
|
||||
emoji_reactions.each do |emoji_reaction|
|
||||
emoji_reaction.destroy!
|
||||
create_notification(emoji_reaction)
|
||||
emoji_reaction
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -3,12 +3,19 @@
|
|||
class EmojiReactionValidator < ActiveModel::Validator
|
||||
SUPPORTED_EMOJIS = Oj.load(File.read(Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json'))).keys.freeze
|
||||
LIMIT = 20
|
||||
MAX_PER_ACCOUNT = 1
|
||||
MAX_PER_ACCOUNT_LIMIT = 20
|
||||
|
||||
def validate(reaction)
|
||||
return if reaction.name.blank?
|
||||
|
||||
reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name)
|
||||
@reaction = reaction
|
||||
|
||||
max_per_account = [MAX_PER_ACCOUNT, Setting.reaction_max_per_account].max
|
||||
|
||||
reaction.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name)
|
||||
reaction.errors.add(:name, I18n.t('reactions.errors.limit_reached', max: max_per_account)) if reaction.account.local? && reaction_per_account >= max_per_account
|
||||
reaction.errors.add(:name, I18n.t('reactions.errors.limit_reached', max: MAX_PER_ACCOUNT_LIMIT)) if !reaction.account.local? && reaction_per_account >= MAX_PER_ACCOUNT_LIMIT
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -16,4 +23,8 @@ class EmojiReactionValidator < ActiveModel::Validator
|
|||
def unicode_emoji?(name)
|
||||
SUPPORTED_EMOJIS.include?(name)
|
||||
end
|
||||
|
||||
def reaction_per_account
|
||||
EmojiReaction.where(account_id: @reaction.account_id, status_id: @reaction.status_id).size
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,6 +98,9 @@
|
|||
.fields-group
|
||||
= f.input :poll_max_options, wrapper: :with_label, label: t('admin.settings.poll_max_options.title'), hint: t('admin.settings.poll_max_options.desc_html', count: PollValidator::MAX_OPTIONS_LIMIT), fedibird_features: true
|
||||
|
||||
.fields-group
|
||||
= f.input :reaction_max_per_account, wrapper: :with_label, label: t('admin.settings.reaction_max_per_account.title'), hint: t('admin.settings.reaction_max_per_account.desc_html', count: EmojiReactionValidator::MAX_PER_ACCOUNT_LIMIT), fedibird_features: true
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnEmojiReactionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(account_id, status_id)
|
||||
UnEmojiReactionService.new.call(Account.find(account_id), Status.find(status_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -667,6 +667,9 @@ en:
|
|||
profile_directory:
|
||||
desc_html: Allow users to be discoverable
|
||||
title: Enable profile directory
|
||||
reaction_max_per_account:
|
||||
desc_html: "Specifies the maximum number of emoji reaction per account (<= %{count})"
|
||||
title: Maximum number of emoji reaction per account
|
||||
registrations:
|
||||
closed_message:
|
||||
desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags
|
||||
|
|
|
@ -646,6 +646,9 @@ ja:
|
|||
profile_directory:
|
||||
desc_html: ユーザーが見つかりやすくできるようになります
|
||||
title: ディレクトリを有効にする
|
||||
reaction_max_per_account:
|
||||
desc_html: "1アカウントあたりの絵文字リアクションの最大数(%{count}以下)を指定します。"
|
||||
title: 絵文字リアクションの最大数
|
||||
registrations:
|
||||
closed_message:
|
||||
desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます
|
||||
|
|
|
@ -366,7 +366,7 @@ Rails.application.routes.draw do
|
|||
resource :pin, only: :create
|
||||
post :unpin, to: 'pins#destroy'
|
||||
|
||||
resources :emoji_reactions, only: :update, constraints: { id: /[^\/]+/ }
|
||||
resources :emoji_reactions, only: [:update, :destroy], constraints: { id: /[^\/]+/ }
|
||||
post :emoji_unreaction, to: 'emoji_reactions#destroy'
|
||||
|
||||
resource :history, only: :show
|
||||
|
|
|
@ -144,6 +144,7 @@ defaults: &defaults
|
|||
hide_video_preview: false
|
||||
allow_poll_image: false
|
||||
poll_max_options: 4
|
||||
reaction_max_per_account: 1
|
||||
|
||||
development:
|
||||
<<: *defaults
|
||||
|
|
Loading…
Reference in a new issue