diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 41f3ce2ee3..210fbbbb49 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 RateLimitHeaders include 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 0000000000..03817c6823 --- /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 41523c945a..88a3ac4644 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 d9d7e14060..d44c535137 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -33,6 +33,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_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'; export const PIN_FAIL = 'PIN_FAIL'; @@ -427,6 +431,41 @@ export function fetchFavouritesFail(id, error) { }; } +export function fetchEmojiReactions(id) { + return (dispatch, getState) => { + dispatch(fetchEmojiReactionsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => { + dispatch(importFetchedAccounts(response.data.map((er) => er.account))); + dispatch(fetchEmojiReactionsSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchEmojiReactionsFail(id, error)); + }); + }; +} + +export function fetchEmojiReactionsRequest(id) { + return { + type: EMOJI_REACTIONS_FETCH_REQUEST, + id, + }; +} + +export function fetchEmojiReactionsSuccess(id, accounts) { + return { + type: EMOJI_REACTIONS_FETCH_SUCCESS, + id, + accounts, + }; +} + +export function fetchEmojiReactionsFail(id, error) { + return { + type: EMOJI_REACTIONS_FETCH_FAIL, + 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 7706c3f88a..6b6fba60cb 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -39,6 +39,7 @@ class Account extends ImmutablePureComponent { actionTitle: PropTypes.string, defaultAction: PropTypes.string, onActionClick: PropTypes.func, + children: PropTypes.object, }; static defaultProps = { @@ -70,7 +71,7 @@ class Account extends ImmutablePureComponent { }; render () { - const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props; + const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, children } = this.props; if (!account) { return ( @@ -146,6 +147,10 @@ class Account extends ImmutablePureComponent { <DisplayName account={account} /> </Link> + <div> + {children} + </div> + <div className='account__relationship'> {buttons} </div> diff --git a/app/javascript/mastodon/components/emoji_view.jsx b/app/javascript/mastodon/components/emoji_view.jsx new file mode 100644 index 0000000000..ac6e92a2ed --- /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 ( + <span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} /> + ); + } + +} diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index ce86040549..6e624a6295 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 ( <button className={classNames(classList)} type='button' onClick={this.onClick}> - <span className='emoji' dangerouslySetInnerHTML={{ __html: emojiHtml }} /> + <span className='emoji'> + <EmojiView name={name} url={url} staticUrl={staticUrl} /> + </span> <span className='count'>{count}</span> </button> ); 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 0000000000..9ea4c11f52 --- /dev/null +++ b/app/javascript/mastodon/features/emoji_reactions/index.jsx @@ -0,0 +1,107 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; +import ColumnHeader from 'mastodon/components/column_header'; +import Icon from 'mastodon/components/icon'; +import { fetchEmojiReactions, fetchFavourites } from 'mastodon/actions/interactions'; +import LoadingIndicator from 'mastodon/components/loading_indicator'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import AccountContainer from 'mastodon/containers/account_container'; +import Column from 'mastodon/features/ui/components/column'; +import { Helmet } from 'react-helmet'; +import EmojiView from '../../components/emoji_view'; + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); + +const mapStateToProps = (state, props) => { return { + accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId]), +} }; + +export default @connect(mapStateToProps) +@injectIntl +class EmojiReactions extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + componentWillMount () { + if (!this.props.accountIds) { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + } + } + + componentWillReceiveProps (nextProps) { + if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { + this.props.dispatch(fetchEmojiReactions(nextProps.params.statusId)); + } + } + + handleRefresh = () => { + this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); + }; + + render () { + const { intl, accountIds, multiColumn } = this.props; + console.dir(accountIds); + + if (!accountIds) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + 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); + } + console.dir(groups) + + const emptyMessage = <FormattedMessage id='empty_column.emoji_reactions' defaultMessage='No one has reacted with emoji this post yet. When someone does, they will show up here.' />; + + return ( + <Column bindToDocument={!multiColumn}> + <ColumnHeader + showBackButton + multiColumn={multiColumn} + extraButton={( + <button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} + /> + + <ScrollableList + scrollKey='emoji_reactions' + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {Object.keys(groups).map((key, index) =>( + <AccountContainer key={index} id={key} withNote={false}> + <div style={ { 'max-width': '100px' } }> + {groups[key].map((value) => <EmojiView name={value.name} url={value.url} staticUrl={value.static_url} />)} + </div> + </AccountContainer> + ))} + </ScrollableList> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index bf77e0da75..942be1b8df 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -49,6 +49,8 @@ class DetailedStatus extends ImmutablePureComponent { available: PropTypes.bool, }), onToggleMediaVisibility: PropTypes.func, + onEmojiReact: PropTypes.func, + onUnEmojiReact: PropTypes.func, }; state = { @@ -191,7 +193,7 @@ class DetailedStatus extends ImmutablePureComponent { let emojiReactionsBar = null; if (status.get('emoji_reactions')) { const emojiReactions = status.get('emoji_reactions'); - emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReaction={this.props.onEmojiReaction} OnUnEmojiReaction={this.props.OnUnEmojiReaction} />; + emojiReactionsBar = <StatusEmojiReactionsBar emojiReactions={emojiReactions} status={status} onEmojiReact={this.props.onEmojiReact} onUnEmojiReact={this.props.onUnEmojiReact} />; } 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 34b88b0ec8..45dd276a6f 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -14,6 +14,7 @@ import { pin, unpin, emojiReact, + unEmojiReact, } from '../../../actions/interactions'; import { muteStatus, @@ -98,6 +99,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 2c6728fc05..161d105451 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -24,6 +24,8 @@ import Column from '../ui/components/column'; import { favourite, unfavourite, + emojiReact, + unEmojiReact, bookmark, unbookmark, reblog, @@ -254,6 +256,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)); @@ -644,6 +656,8 @@ class Status extends ImmutablePureComponent { showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} pictureInPicture={pictureInPicture} + onEmojiReact={this.handleEmojiReact} + onUnEmojiReact={this.handleUnEmojiReact} /> <ActionBar @@ -651,6 +665,7 @@ class Status extends ImmutablePureComponent { status={status} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} + onEmojiReact={this.handleEmojiReact} onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4f0ea04504..dc8ed444a5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -36,6 +36,7 @@ import { Following, Reblogs, Favourites, + EmojiReactions, DirectTimeline, HashtagTimeline, Notifications, @@ -206,6 +207,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} /> + <WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} /> {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> @@ -213,6 +215,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> + <WrappedRoute path='/statuses/:statusId/emoji_reactions' component={EmojiReactions} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} /> diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 1cf07f6453..5e175c9413 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -78,6 +78,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 2a80cf6398..b57cbe3823 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -26,6 +26,7 @@ import { import { REBLOGS_FETCH_SUCCESS, FAVOURITES_FETCH_SUCCESS, + EMOJI_REACTIONS_FETCH_SUCCESS, } from '../actions/interactions'; import { BLOCKS_FETCH_REQUEST, @@ -69,6 +70,7 @@ const initialState = ImmutableMap({ following: initialListState, reblogged_by: initialListState, favourited_by: initialListState, + emoji_reactioned_by: initialListState, follow_requests: initialListState, blocks: initialListState, mutes: initialListState, @@ -133,6 +135,10 @@ export default function userLists(state = initialState, action) { return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); + case EMOJI_REACTIONS_FETCH_SUCCESS: + console.log('===================') + console.dir(state); + return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts)); case NOTIFICATIONS_UPDATE: return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; case FOLLOW_REQUESTS_FETCH_SUCCESS: diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index db3cb91b3f..df8cea913d 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 09a7ac7a30..a8558f457d 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 4dbeab1946..b66751ffed 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 0000000000..1925754077 --- /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 a2d535d262..b1e0833a56 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 cf746bb0ef..d8f51548a0 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 ca58c3c1d0..5a2e9eaf1c 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 9b79366b74..55654695d0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,7 @@ Rails.application.routes.draw do /lists/(*any) /notifications /favourites + /emoji_reactions /bookmarks /pinned /start @@ -439,6 +440,7 @@ Rails.application.routes.draw 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'