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'