Add emoji reaction detail status

This commit is contained in:
KMY 2023-02-26 23:44:52 +09:00
parent de951a0ef9
commit a1485f242d
22 changed files with 393 additions and 16 deletions

View file

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::BaseController < ApplicationController class Api::BaseController < ApplicationController
DEFAULT_STATUSES_LIMIT = 20 DEFAULT_STATUSES_LIMIT = 20
DEFAULT_ACCOUNTS_LIMIT = 40 DEFAULT_ACCOUNTS_LIMIT = 40
DEFAULT_EMOJI_REACTION_LIMIT = 10
include RateLimitHeaders include RateLimitHeaders
include AccessTokenTrackingConcern include AccessTokenTrackingConcern

View file

@ -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

View file

@ -40,6 +40,13 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
private private
def create_private(emoji) 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) EmojiReactService.new.call(current_account, @status, emoji)
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end

View file

@ -33,6 +33,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; 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_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS'; export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL'; 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) { export function pin(status) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(pinRequest(status)); dispatch(pinRequest(status));

View file

@ -39,6 +39,7 @@ class Account extends ImmutablePureComponent {
actionTitle: PropTypes.string, actionTitle: PropTypes.string,
defaultAction: PropTypes.string, defaultAction: PropTypes.string,
onActionClick: PropTypes.func, onActionClick: PropTypes.func,
children: PropTypes.object,
}; };
static defaultProps = { static defaultProps = {
@ -70,7 +71,7 @@ class Account extends ImmutablePureComponent {
}; };
render () { 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) { if (!account) {
return ( return (
@ -146,6 +147,10 @@ class Account extends ImmutablePureComponent {
<DisplayName account={account} /> <DisplayName account={account} />
</Link> </Link>
<div>
{children}
</div>
<div className='account__relationship'> <div className='account__relationship'>
{buttons} {buttons}
</div> </div>

View file

@ -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 }} />
);
}
}

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import emojify from '../features/emoji/emoji'; import emojify from '../features/emoji/emoji';
import classNames from 'classnames'; import classNames from 'classnames';
import EmojiView from './emoji_view';
class EmojiReactionButton extends React.PureComponent { class EmojiReactionButton extends React.PureComponent {
@ -32,15 +33,6 @@ class EmojiReactionButton extends React.PureComponent {
render () { render () {
const { name, url, staticUrl, count, me } = this.props; 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 = { const classList = {
'emoji-reactions-bar__button': true, 'emoji-reactions-bar__button': true,
'toggled': me, 'toggled': me,
@ -48,7 +40,9 @@ class EmojiReactionButton extends React.PureComponent {
return ( return (
<button className={classNames(classList)} type='button' onClick={this.onClick}> <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> <span className='count'>{count}</span>
</button> </button>
); );

View file

@ -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>
);
}
}

View file

@ -49,6 +49,8 @@ class DetailedStatus extends ImmutablePureComponent {
available: PropTypes.bool, available: PropTypes.bool,
}), }),
onToggleMediaVisibility: PropTypes.func, onToggleMediaVisibility: PropTypes.func,
onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func,
}; };
state = { state = {
@ -191,7 +193,7 @@ class DetailedStatus extends ImmutablePureComponent {
let emojiReactionsBar = null; let emojiReactionsBar = null;
if (status.get('emoji_reactions')) { if (status.get('emoji_reactions')) {
const emojiReactions = 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')) { if (status.get('application')) {

View file

@ -14,6 +14,7 @@ import {
pin, pin,
unpin, unpin,
emojiReact, emojiReact,
unEmojiReact,
} from '../../../actions/interactions'; } from '../../../actions/interactions';
import { import {
muteStatus, muteStatus,
@ -98,6 +99,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(emojiReact(status, emoji)); dispatch(emojiReact(status, emoji));
}, },
onUnEmojiReact (status, emoji) {
dispatch(unEmojiReact(status, emoji));
},
onPin (status) { onPin (status) {
if (status.get('pinned')) { if (status.get('pinned')) {
dispatch(unpin(status)); dispatch(unpin(status));

View file

@ -24,6 +24,8 @@ import Column from '../ui/components/column';
import { import {
favourite, favourite,
unfavourite, unfavourite,
emojiReact,
unEmojiReact,
bookmark, bookmark,
unbookmark, unbookmark,
reblog, 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) => { handlePin = (status) => {
if (status.get('pinned')) { if (status.get('pinned')) {
this.props.dispatch(unpin(status)); this.props.dispatch(unpin(status));
@ -644,6 +656,8 @@ class Status extends ImmutablePureComponent {
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture} pictureInPicture={pictureInPicture}
onEmojiReact={this.handleEmojiReact}
onUnEmojiReact={this.handleUnEmojiReact}
/> />
<ActionBar <ActionBar
@ -651,6 +665,7 @@ class Status extends ImmutablePureComponent {
status={status} status={status}
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onEmojiReact={this.handleEmojiReact}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}

View file

@ -36,6 +36,7 @@ import {
Following, Following,
Reblogs, Reblogs,
Favourites, Favourites,
EmojiReactions,
DirectTimeline, DirectTimeline,
HashtagTimeline, HashtagTimeline,
Notifications, Notifications,
@ -206,6 +207,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} 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 */} {/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> <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' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} 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='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} />

View file

@ -78,6 +78,10 @@ export function Favourites () {
return import(/* webpackChunkName: "features/favourites" */'../../favourites'); return import(/* webpackChunkName: "features/favourites" */'../../favourites');
} }
export function EmojiReactions () {
return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions');
}
export function FollowRequests () { export function FollowRequests () {
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
} }

View file

@ -26,6 +26,7 @@ import {
import { import {
REBLOGS_FETCH_SUCCESS, REBLOGS_FETCH_SUCCESS,
FAVOURITES_FETCH_SUCCESS, FAVOURITES_FETCH_SUCCESS,
EMOJI_REACTIONS_FETCH_SUCCESS,
} from '../actions/interactions'; } from '../actions/interactions';
import { import {
BLOCKS_FETCH_REQUEST, BLOCKS_FETCH_REQUEST,
@ -69,6 +70,7 @@ const initialState = ImmutableMap({
following: initialListState, following: initialListState,
reblogged_by: initialListState, reblogged_by: initialListState,
favourited_by: initialListState, favourited_by: initialListState,
emoji_reactioned_by: initialListState,
follow_requests: initialListState, follow_requests: initialListState,
blocks: initialListState, blocks: initialListState,
mutes: 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))); return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
case FAVOURITES_FETCH_SUCCESS: case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); 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: case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS: case FOLLOW_REQUESTS_FETCH_SUCCESS:

View file

@ -42,8 +42,11 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
return if @account.reacted?(@original_status, shortcode, emoji) 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 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']) reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id'])
write_stream(reaction)
if @original_status.account.local? if @original_status.account.local?
NotifyService.new.call(@original_status.account, :emoji_reaction, reaction) 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'] @emoji_tag = @json['tag'].is_a?(Array) ? @json['tag']&.first : @json['tag']
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) }
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 end

View file

@ -140,6 +140,23 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
end end
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 def forward_for_undo_emoji_reaction
return unless @json['signature'].present? return unless @json['signature'].present?

View file

@ -40,4 +40,11 @@ class EmojiReaction < ApplicationRecord
account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav) account.statuses_cleanup_policy&.invalidate_last_inspected(status, :unfav)
end 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 end

View file

@ -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

View file

@ -147,6 +147,7 @@ class DeleteAccountService < BaseService
purge_polls! purge_polls!
purge_generated_notifications! purge_generated_notifications!
purge_favourites! purge_favourites!
purge_emoji_reactions!
purge_bookmarks! purge_bookmarks!
purge_feeds! purge_feeds!
purge_other_associations! purge_other_associations!
@ -193,6 +194,16 @@ class DeleteAccountService < BaseService
end end
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! def purge_bookmarks!
@account.bookmarks.in_batches do |bookmarks| @account.bookmarks.in_batches do |bookmarks|
Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled? Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled?

View file

@ -55,7 +55,7 @@ class EmojiReactService < BaseService
def write_stream(emoji_reaction) def write_stream(emoji_reaction)
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name 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 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) FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id)
end end

View file

@ -40,7 +40,7 @@ class UnEmojiReactService < BaseService
def write_stream(emoji_reaction) def write_stream(emoji_reaction)
emoji_group = @status.emoji_reactions_grouped_by_name 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 if emoji_group
emoji_group['status_id'] = @status.id.to_s emoji_group['status_id'] = @status.id.to_s
else else

View file

@ -16,6 +16,7 @@ Rails.application.routes.draw do
/lists/(*any) /lists/(*any)
/notifications /notifications
/favourites /favourites
/emoji_reactions
/bookmarks /bookmarks
/pinned /pinned
/start /start
@ -439,6 +440,7 @@ Rails.application.routes.draw do
scope module: :statuses do scope module: :statuses do
resources :reblogged_by, controller: :reblogged_by_accounts, only: :index resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
resources :favourited_by, controller: :favourited_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 resource :reblog, only: :create
post :unreblog, to: 'reblogs#destroy' post :unreblog, to: 'reblogs#destroy'