diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 98fa1897e..ace2459aa 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true class Api::BaseController < ApplicationController - DEFAULT_STATUSES_LIMIT = 20 - DEFAULT_ACCOUNTS_LIMIT = 40 + DEFAULT_STATUSES_LIMIT = 20 + DEFAULT_ACCOUNTS_LIMIT = 40 + DEFAULT_EMOJI_REACTION_LIMIT = 10 include Api::RateLimitHeaders include Api::AccessTokenTrackingConcern diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb new file mode 100644 index 000000000..03817c682 --- /dev/null +++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseController + include Authorization + + before_action -> { authorize_if_got_token! :read, :'read:accounts' } + before_action :set_status + after_action :insert_pagination_headers + + def index + @accounts = load_accounts + render json: @accounts, each_serializer: REST::EmojiReactionAccountSerializer + end + + private + + def load_accounts + scope = default_accounts + # scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? + scope.merge(paginated_emoji_reactions).to_a + end + + def default_accounts + EmojiReaction + .where(status_id: @status.id) + #.where(account: { suspended_at: nil }) + end + + def paginated_emoji_reactions + EmojiReaction.paginate_by_max_id( + limit_param(1000), #limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_status_emoji_reactioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_status_emoji_reactioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end +end diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 41523c945..88a3ac464 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -40,6 +40,13 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController private def create_private(emoji) + count = EmojiReaction.where(account: current_account, status: @status).count + + if count >= DEFAULT_EMOJI_REACTION_LIMIT + bad_request + return + end + EmojiReactService.new.call(current_account, @status, emoji) render json: @status, serializer: REST::StatusSerializer end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index a578cef03..d34eb5c8c 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -41,7 +41,11 @@ export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST'; export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS'; -export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; +export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL'; + +export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST'; +export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS'; +export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL'; export const PIN_REQUEST = 'PIN_REQUEST'; export const PIN_SUCCESS = 'PIN_SUCCESS'; @@ -531,6 +535,85 @@ export function expandFavouritesFail(id, error) { }; } +export function fetchEmojiReactions(id) { + return (dispatch, getState) => { + dispatch(fetchEmojiReactionsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data.map((er) => er.account))); + dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchEmojiReactionsFail(id, error)); + }); + }; +} + +export function fetchEmojiReactionsRequest(id) { + return { + type: EMOJI_REACTIONS_FETCH_REQUEST, + id, + }; +} + +export function fetchEmojiReactionsSuccess(id, accounts, next) { + return { + type: EMOJI_REACTIONS_FETCH_SUCCESS, + id, + accounts, + next, + }; +} + +export function fetchEmojiReactionsFail(id, error) { + return { + type: EMOJI_REACTIONS_FETCH_FAIL, + error, + }; +} + +export function expandEmojiReactions(id) { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next']); + if (url === null) { + return; + } + + dispatch(expandEmojiReactionsRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map((er) => er.account))); + dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); + }).catch(error => dispatch(expandEmojiReactionsFail(id, error))); + }; +} + +export function expandEmojiReactionsRequest(id) { + return { + type: EMOJI_REACTIONS_EXPAND_REQUEST, + id, + }; +} + +export function expandEmojiReactionsSuccess(id, accounts, next) { + return { + type: EMOJI_REACTIONS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +} + +export function expandEmojiReactionsFail(id, error) { + return { + type: EMOJI_REACTIONS_EXPAND_FAIL, + id, + error, + }; +} + export function pin(status) { return (dispatch, getState) => { dispatch(pinRequest(status)); diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index f82dd9153..7519a1347 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -46,6 +46,8 @@ class Account extends ImmutablePureComponent { minimal: PropTypes.bool, defaultAction: PropTypes.string, withBio: PropTypes.bool, + onActionClick: PropTypes.func, + children: PropTypes.object, }; static defaultProps = { @@ -73,7 +75,7 @@ class Account extends ImmutablePureComponent { }; render () { - const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props; + const { account, intl, hidden, onActionClick, actionIcon, actionTitle, withBio, defaultAction, size, minimal, children } = this.props; if (!account) { return ; @@ -156,11 +158,13 @@ class Account extends ImmutablePureComponent { - {!minimal && ( -
- {buttons} -
- )} +
+ {children} +
+ +
+ {buttons} +
{withBio && (account.get('note').length > 0 ? ( diff --git a/app/javascript/mastodon/components/emoji_view.jsx b/app/javascript/mastodon/components/emoji_view.jsx new file mode 100644 index 000000000..ac6e92a2e --- /dev/null +++ b/app/javascript/mastodon/components/emoji_view.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; +import emojify from '../features/emoji/emoji'; +import classNames from 'classnames'; + +export default class EmojiView extends React.PureComponent { + + static propTypes = { + name: PropTypes.string, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render () { + const { name, url, staticUrl } = this.props; + + let emojiHtml = null; + if (url) { + let customEmojis = {}; + customEmojis[`:${name}:`] = { url, static_url: staticUrl }; + emojiHtml = emojify(`:${name}:`, customEmojis); + } else { + emojiHtml = emojify(name); + } + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index 224eed615..ce0741c0c 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { injectIntl } from 'react-intl'; import emojify from '../features/emoji/emoji'; import classNames from 'classnames'; +import EmojiView from './emoji_view'; class EmojiReactionButton extends React.PureComponent { @@ -32,15 +33,6 @@ class EmojiReactionButton extends React.PureComponent { render () { const { name, url, staticUrl, count, me } = this.props; - let emojiHtml = null; - if (url) { - let customEmojis = {}; - customEmojis[`:${name}:`] = { url, static_url: staticUrl }; - emojiHtml = emojify(`:${name}:`, customEmojis); - } else { - emojiHtml = emojify(name); - } - const classList = { 'emoji-reactions-bar__button': true, 'toggled': me, @@ -48,7 +40,9 @@ class EmojiReactionButton extends React.PureComponent { return ( ); diff --git a/app/javascript/mastodon/features/emoji_reactions/index.jsx b/app/javascript/mastodon/features/emoji_reactions/index.jsx new file mode 100644 index 000000000..4f5c3987f --- /dev/null +++ b/app/javascript/mastodon/features/emoji_reactions/index.jsx @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { ReactComponent as RefreshIcon } from '@material-symbols/svg-600/outlined/refresh.svg'; +import { debounce } from 'lodash'; + +import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions'; +import ColumnHeader from 'mastodon/components/column_header'; +import { Icon } from 'mastodon/components/icon'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import AccountContainer from 'mastodon/containers/account_container'; +import Column from 'mastodon/features/ui/components/column'; + + + +import EmojiView from '../../components/emoji_view'; +import { LoadingIndicator } from '../../components/loading_indicator'; + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); + +const mapStateToProps = (state, props) => { + return { + accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']), + hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']), + isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true), + }; +}; + +class EmojiReactions extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + componentWillMount () { + if (!this.props.accountIds) { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + } + } + + handleRefresh = () => { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + }; + + handleLoadMore = debounce(() => { + this.props.dispatch(expandEmojiReactions(this.props.params.statusId)); + }, 300, { leading: true }); + + render () { + const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props; + + if (!accountIds) { + return ( + + + + ); + } + + let groups = {}; + for (const emoji_reaction of accountIds) { + const key = emoji_reaction.account_id; + const value = emoji_reaction; + if (!groups[key]) groups[key] = [value]; + else groups[key].push(value); + } + + const emptyMessage = ; + + return ( + + + )} + /> + + + {Object.keys(groups).map((key) =>( + +
+ {groups[key].map((value, index2) => )} +
+
+ ))} +
+ + + + +
+ ); + } + +} + +export default connect(mapStateToProps)(injectIntl(EmojiReactions)); \ No newline at end of file diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 2cff3de86..b4dd18213 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -55,6 +55,8 @@ class DetailedStatus extends ImmutablePureComponent { onToggleMediaVisibility: PropTypes.func, onQuoteToggleMediaVisibility: PropTypes.func, ...WithRouterPropTypes, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, }; state = { @@ -260,7 +262,7 @@ class DetailedStatus extends ImmutablePureComponent { let emojiReactionsBar = null; if (status.get('emoji_reactions')) { const emojiReactions = status.get('emoji_reactions'); - emojiReactionsBar = ; + emojiReactionsBar = ; } if (status.get('application')) { diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index cd76f0251..a472a085c 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -18,6 +18,7 @@ import { pin, unpin, emojiReact, + unEmojiReact, } from '../../../actions/interactions'; import { openModal } from '../../../actions/modal'; import { initMuteModal } from '../../../actions/mutes'; @@ -105,6 +106,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(emojiReact(status, emoji)); }, + onUnEmojiReact (status, emoji) { + dispatch(unEmojiReact(status, emoji)); + }, + onPin (status) { if (status.get('pinned')) { dispatch(unpin(status)); diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 9e82a3dcc..7847c98ca 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -41,6 +41,8 @@ import { import { favourite, unfavourite, + emojiReact, + unEmojiReact, bookmark, unbookmark, reblog, @@ -279,6 +281,16 @@ class Status extends ImmutablePureComponent { } }; + handleEmojiReact = (status, emoji) => { + const { dispatch } = this.props; + dispatch(emojiReact(status, emoji)); + }; + + handleUnEmojiReact = (status, emoji) => { + const { dispatch } = this.props; + dispatch(unEmojiReact(status, emoji)); + }; + handlePin = (status) => { if (status.get('pinned')) { this.props.dispatch(unpin(status)); @@ -746,6 +758,8 @@ class Status extends ImmutablePureComponent { onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility} pictureInPicture={pictureInPicture} + onEmojiReact={this.handleEmojiReact} + onUnEmojiReact={this.handleUnEmojiReact} /> + @@ -224,6 +226,7 @@ class SwitchingColumnsArea extends PureComponent { + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 7b968204b..2dba7de7d 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -82,6 +82,10 @@ export function Favourites () { return import(/* webpackChunkName: "features/favourites" */'../../favourites'); } +export function EmojiReactions () { + return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions'); +} + export function FollowRequests () { return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); } diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 2f17fed5f..ad626c1b2 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -1,5 +1,4 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - import { DIRECTORY_FETCH_REQUEST, DIRECTORY_FETCH_SUCCESS, @@ -57,6 +56,7 @@ import { FAVOURITES_EXPAND_REQUEST, FAVOURITES_EXPAND_SUCCESS, FAVOURITES_EXPAND_FAIL, + EMOJI_REACTIONS_FETCH_SUCCESS, } from '../actions/interactions'; import { MUTES_FETCH_REQUEST, @@ -79,6 +79,7 @@ const initialState = ImmutableMap({ following: initialListState, reblogged_by: initialListState, favourited_by: initialListState, + emoji_reactioned_by: initialListState, follow_requests: initialListState, blocks: initialListState, mutes: initialListState, @@ -161,6 +162,10 @@ export default function userLists(state = initialState, action) { return state.setIn(['favourited_by', action.id, 'isLoading'], false); case notificationsUpdate.type: return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state; + case EMOJI_REACTIONS_FETCH_SUCCESS: + console.log('===================') + console.dir(state); + return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts)); case FOLLOW_REQUESTS_FETCH_SUCCESS: return normalizeList(state, ['follow_requests'], action.accounts, action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index db3cb91b3..df8cea913 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -42,8 +42,11 @@ class ActivityPub::Activity::Like < ActivityPub::Activity return if @account.reacted?(@original_status, shortcode, emoji) + return if EmojiReaction.where(account: @account, status: @original_status).count >= BaseController::DEFAULT_EMOJI_REACTION_LIMIT + EmojiReaction.find_by(account: @account, status: @original_status)&.destroy reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id']) + write_stream(reaction) if @original_status.account.local? NotifyService.new.call(@original_status.account, :emoji_reaction, reaction) @@ -91,4 +94,15 @@ class ActivityPub::Activity::Like < ActivityPub::Activity @emoji_tag = @json['tag'].is_a?(Array) ? @json['tag']&.first : @json['tag'] end + + def write_stream(emoji_reaction) + emoji_group = @original_status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } + emoji_group['status_id'] = @original_status.id.to_s + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) + end + + def render_emoji_reaction(emoji_group) + @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + end end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index 09a7ac7a3..a8558f457 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -140,6 +140,23 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity end end + def write_stream(emoji_reaction) + emoji_group = @original_status.emoji_reactions_grouped_by_name + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } + if emoji_group + emoji_group['status_id'] = @original_status.id.to_s + else + # name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain + emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @status.id.to_s } + emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji + end + FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @original_status.id, emoji_reaction.account_id) + end + + def render_emoji_reaction(emoji_group) + @render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) + end + def forward_for_undo_emoji_reaction return unless @json['signature'].present? diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 4dbeab194..b66751ffe 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -40,4 +40,11 @@ class EmojiReaction < ApplicationRecord account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) end + + def paginate_by_max_id(limit, max_id = nil, since_id = nil) + query = order(arel_table[:id].desc).limit(limit) + query = query.where(arel_table[:id].lt(max_id)) if max_id.present? + query = query.where(arel_table[:id].gt(since_id)) if since_id.present? + query + end end diff --git a/app/serializers/rest/emoji_reaction_account_serializer.rb b/app/serializers/rest/emoji_reaction_account_serializer.rb new file mode 100644 index 000000000..192575407 --- /dev/null +++ b/app/serializers/rest/emoji_reaction_account_serializer.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class REST::EmojiReactionAccountSerializer < ActiveModel::Serializer + include RoutingHelper + include FormattingHelper + + attributes :id, :name + + attribute :url, if: :custom_emoji? + attribute :static_url, if: :custom_emoji? + attribute :domain, if: :custom_emoji? + + belongs_to :account, serializer: REST::AccountSerializer + + def id + object.id.to_s + end + + def url + full_asset_url(object.custom_emoji.image.url) + end + + def static_url + full_asset_url(object.custom_emoji.image.url(:static)) + end + + def domain + object.custom_emoji.domain + end + + def custom_emoji? + object.custom_emoji.present? + end +end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 7c7cb97df..9dda1631e 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -147,6 +147,7 @@ class DeleteAccountService < BaseService purge_polls! purge_generated_notifications! purge_favourites! + purge_emoji_reactions! purge_bookmarks! purge_feeds! purge_other_associations! @@ -193,6 +194,16 @@ class DeleteAccountService < BaseService end end + def purge_emoji_reactions! + @account.emoji_reactions.in_batches do |reactions| + reactions.each do |reaction| + reaction.status.refresh_emoji_reactions_grouped_by_name + end + Chewy.strategy.current.update(StatusesIndex, reactions.pluck(:status_id)) if Chewy.enabled? + reactions.delete_all + end + end + def purge_bookmarks! @account.bookmarks.in_batches do |bookmarks| Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled? diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index cf746bb0e..d8f51548a 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -55,7 +55,7 @@ class EmojiReactService < BaseService def write_stream(emoji_reaction) emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name - .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } emoji_group['status_id'] = emoji_reaction.status_id.to_s FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id) end diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb index ca58c3c1d..5a2e9eaf1 100644 --- a/app/services/un_emoji_react_service.rb +++ b/app/services/un_emoji_react_service.rb @@ -40,7 +40,7 @@ class UnEmojiReactService < BaseService def write_stream(emoji_reaction) emoji_group = @status.emoji_reactions_grouped_by_name - .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } + .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) } if emoji_group emoji_group['status_id'] = @status.id.to_s else diff --git a/config/routes.rb b/config/routes.rb index 85c3b1855..414fb81a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ Rails.application.routes.draw do /lists/(*any) /notifications /favourites + /emoji_reactions /bookmarks /pinned /start/(*any) diff --git a/config/routes/api.rb b/config/routes/api.rb index ca6ab1a29..1ea8f4712 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -10,6 +10,7 @@ namespace :api, format: false do scope module: :statuses do resources :reblogged_by, controller: :reblogged_by_accounts, only: :index resources :favourited_by, controller: :favourited_by_accounts, only: :index + resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index resource :reblog, only: :create post :unreblog, to: 'reblogs#destroy'