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