Add blocking and muting process for emoji reactions

This commit is contained in:
noellabo 2021-09-18 15:11:36 +09:00
parent 56fa09dbd8
commit a63147aa0b
6 changed files with 120 additions and 83 deletions

View file

@ -0,0 +1,83 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import ImmutablePropTypes, { list } from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Avatar from 'mastodon/components/avatar';
import { FormattedMessage, injectIntl } from 'react-intl';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
});
return mapStateToProps;
};
@connect(makeMapStateToProps)
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
};
render () {
const { account } = this.props;
if ( !account ) {
return <Fragment></Fragment>;
}
return (
<div className='account-popup__wapper'>
<div className='account-popup__avatar-wrapper'><Avatar account={account} size={14} /></div>
<bdi><strong className='account-popup__display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
</div>
);
}
}
const ACCOUNT_POPUP_ROWS_MAX = 10;
@injectIntl
export default class AccountPopup extends ImmutablePureComponent {
static propTypes = {
accountIds: ImmutablePropTypes.list.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
};
render () {
const { accountIds, placement } = this.props;
var { arrowOffsetLeft, arrowOffsetTop, style } = this.props;
const OFFSET = 6;
if (placement === 'top') {
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') - OFFSET);
style = { ...style, top: style.top - OFFSET };
} else if (placement === 'bottom') {
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') + OFFSET);
style = { ...style, top: style.top + OFFSET };
} else if (placement === 'left') {
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') - OFFSET);
style = { ...style, left: style.left - OFFSET };
} else if (placement === 'right') {
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') + OFFSET);
style = { ...style, left: style.left + OFFSET };
}
return (
<div className={`dropdown-menu account-popup ${placement}`} style={{ ...style}}>
<div className={`dropdown-menu__arrow account-popup__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
{accountIds.take(ACCOUNT_POPUP_ROWS_MAX).map(accountId => <Account key={accountId} accountId={accountId} />)}
{accountIds.size > ACCOUNT_POPUP_ROWS_MAX && <div className='account-popup__wapper'><bdi><strong className='account-popup__display-name__html'><FormattedMessage id='account_popup.more_users' defaultMessage='({number, plural, one {# other user} other {# other users}})' values={{ number: accountIds.size - ACCOUNT_POPUP_ROWS_MAX}} children={msg=> <>{msg}</>} /></strong></bdi></div>}
</div>
);
}
}

View file

@ -1,8 +1,7 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePropTypes, { list } 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 classNames from 'classnames';
@ -12,91 +11,47 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import AnimatedNumber from 'mastodon/components/animated_number'; import AnimatedNumber from 'mastodon/components/animated_number';
import { reduceMotion, me } from 'mastodon/initial_state'; import { reduceMotion, me } from 'mastodon/initial_state';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import Avatar from 'mastodon/components/avatar';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/lib/Overlay';
import { FormattedMessage, injectIntl } from 'react-intl';
import { isUserTouching } from 'mastodon/is_mobile'; import { isUserTouching } from 'mastodon/is_mobile';
import AccountPopup from 'mastodon/components/account_popup';
const makeMapStateToProps = () => { const getFilteredEmojiReaction = (emojiReaction, relationships) => {
const getAccount = makeGetAccount(); 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, { accountId }) => ({ const count = filteredEmojiReaction.get('account_ids').size;
account: getAccount(state, accountId),
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),
}); });
return mapStateToProps; @connect(mapStateToProps, null, mergeProps)
};
@connect(makeMapStateToProps)
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
};
render () {
const { account } = this.props;
if ( !account ) {
return <Fragment></Fragment>;
}
return (
<div className='account-popup__wapper'>
<div className='account-popup__avatar-wrapper'><Avatar account={account} size={14} /></div>
<bdi><strong className='account-popup__display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
</div>
);
}
}
const ACCOUNT_POPUP_ROWS_MAX = 10;
@injectIntl
class AccountPopup extends ImmutablePureComponent {
static propTypes = {
accountIds: ImmutablePropTypes.list.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
};
render () {
const { accountIds, placement } = this.props;
var { arrowOffsetLeft, arrowOffsetTop, style } = this.props;
const OFFSET = 6;
if (placement === 'top') {
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') - OFFSET);
style = { ...style, top: style.top - OFFSET };
} else if (placement === 'bottom') {
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') + OFFSET);
style = { ...style, top: style.top + OFFSET };
} else if (placement === 'left') {
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') - OFFSET);
style = { ...style, left: style.left - OFFSET };
} else if (placement === 'right') {
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') + OFFSET);
style = { ...style, left: style.left + OFFSET };
}
return (
<div className={`dropdown-menu account-popup ${placement}`} style={{ ...style}}>
<div className={`dropdown-menu__arrow account-popup__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
{accountIds.take(ACCOUNT_POPUP_ROWS_MAX).map(accountId => <Account key={accountId} accountId={accountId} />)}
{accountIds.size > ACCOUNT_POPUP_ROWS_MAX && <div className='account-popup__wapper'><bdi><strong className='account-popup__display-name__html'><FormattedMessage id='account_popup.more_users' defaultMessage='({number, plural, one {# other user} other {# other users}})' values={{ number: accountIds.size - ACCOUNT_POPUP_ROWS_MAX}} children={msg=> <>{msg}</>} /></strong></bdi></div>}
</div>
);
}
}
class EmojiReaction extends ImmutablePureComponent { class EmojiReaction extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
emojiReaction: ImmutablePropTypes.map.isRequired, emojiReaction: ImmutablePropTypes.map,
myReaction: PropTypes.bool.isRequired, myReaction: PropTypes.bool.isRequired,
addEmojiReaction: PropTypes.func.isRequired, addEmojiReaction: PropTypes.func.isRequired,
removeEmojiReaction: PropTypes.func.isRequired, removeEmojiReaction: PropTypes.func.isRequired,
@ -154,6 +109,10 @@ class EmojiReaction extends ImmutablePureComponent {
render () { render () {
const { emojiReaction, status, myReaction } = this.props; const { emojiReaction, status, myReaction } = this.props;
if (!emojiReaction) {
return <Fragment></Fragment>;
}
let shortCode = emojiReaction.get('name'); let shortCode = emojiReaction.get('name');
if (unicodeMapping[shortCode]) { if (unicodeMapping[shortCode]) {
@ -198,7 +157,7 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
render () { render () {
const { status } = this.props; const { status } = this.props;
const emoji_reactions = status.get("emoji_reactions") const emoji_reactions = status.get("emoji_reactions");
const visibleReactions = emoji_reactions.filter(x => x.get('count') > 0); const visibleReactions = emoji_reactions.filter(x => x.get('count') > 0);
if (visibleReactions.isEmpty() ) { if (visibleReactions.isEmpty() ) {

View file

@ -6,6 +6,7 @@ class ActivityPub::Activity::EmojiReact < ActivityPub::Activity
shortcode = @json['content'] shortcode = @json['content']
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.reacted?(original_status, shortcode) return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.reacted?(original_status, shortcode)
return if original_status.account.blocking?(@account) || @account.blocking?(original_status.account) || original_status.account.domain_blocking?(@account.domain)
reaction = original_status.emoji_reactions.create!(account: @account, name: shortcode, uri: @json['id']) reaction = original_status.emoji_reactions.create!(account: @account, name: shortcode, uri: @json['id'])

View file

@ -5,6 +5,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
@original_status = status_from_uri(object_uri) @original_status = status_from_uri(object_uri)
return if @original_status.nil? || delete_arrived_first?(@json['id']) return if @original_status.nil? || delete_arrived_first?(@json['id'])
return if @original_status.account.local? && (@original_status.account.blocking?(@account) || @account.blocking?(@original_status.account) || @original_status.account.domain_blocking?(@account.domain))
lock_or_fail("like:#{object_uri}") do lock_or_fail("like:#{object_uri}") do
if shortcode.nil? if shortcode.nil?

View file

@ -304,13 +304,15 @@ class Status < ApplicationRecord
if account.present? if account.present?
emoji_reactions.each do |emoji_reaction| emoji_reactions.each do |emoji_reaction|
emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s) emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s)
emoji_reaction['account_ids'] -= account.excluded_from_timeline_account_ids.map(&:to_s)
emoji_reaction['count'] = emoji_reaction['account_ids'].size
end end
end end
end end
end end
def generate_grouped_emoji_reactions def generate_grouped_emoji_reactions
records = emoji_reactions.group(:name).order(Arel.sql('MIN(created_at) ASC')).select('name, min(custom_emoji_id) as custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids') records = emoji_reactions.group(:name).order(Arel.sql('MIN(created_at) ASC')).select('name, min(custom_emoji_id) as custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids').limit(EmojiReactionValidator::LIMIT)
Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::GroupedEmojiReactionSerializer, scope: nil, scope_name: :current_user)) Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::GroupedEmojiReactionSerializer, scope: nil, scope_name: :current_user))
end end

View file

@ -8,7 +8,6 @@ class EmojiReactionValidator < ActiveModel::Validator
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.errors.add(:name, I18n.t('reactions.errors.unrecognized_emoji')) if reaction.custom_emoji_id.blank? && !unicode_emoji?(reaction.name)
reaction.errors.add(:base, I18n.t('reactions.errors.limit_reached')) if new_reaction?(reaction) && limit_reached?(reaction)
end end
@ -17,12 +16,4 @@ class EmojiReactionValidator < ActiveModel::Validator
def unicode_emoji?(name) def unicode_emoji?(name)
SUPPORTED_EMOJIS.include?(name) SUPPORTED_EMOJIS.include?(name)
end end
def new_reaction?(reaction)
!reaction.status.emoji_reactions.where(name: reaction.name).exists?
end
def limit_reached?(reaction)
reaction.status.emoji_reactions.where.not(name: reaction.name).count('distinct name') >= LIMIT
end
end end