Add blocking and muting process for emoji reactions
This commit is contained in:
parent
56fa09dbd8
commit
a63147aa0b
6 changed files with 120 additions and 83 deletions
83
app/javascript/mastodon/components/account_popup.js
Normal file
83
app/javascript/mastodon/components/account_popup.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
if (count > 0) {
|
||||||
|
return filteredEmojiReaction.set('count', count);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@connect(makeMapStateToProps)
|
const mapStateToProps = (state, { emojiReaction }) => {
|
||||||
class Account extends ImmutablePureComponent {
|
const relationship = new Map();
|
||||||
|
emojiReaction.get('account_ids').forEach(accountId => relationship.set(accountId, state.getIn(['relationships', accountId])));
|
||||||
|
|
||||||
static propTypes = {
|
return {
|
||||||
account: ImmutablePropTypes.map,
|
emojiReaction: emojiReaction,
|
||||||
|
relationships: relationship,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
const mergeProps = ({ emojiReaction, relationships }, dispatchProps, ownProps) => ({
|
||||||
const { account } = this.props;
|
...ownProps,
|
||||||
|
...dispatchProps,
|
||||||
if ( !account ) {
|
emojiReaction: getFilteredEmojiReaction(emojiReaction, relationships),
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@connect(mapStateToProps, null, mergeProps)
|
||||||
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() ) {
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue