Add emoji reaction detail status
This commit is contained in:
parent
de951a0ef9
commit
a1485f242d
22 changed files with 393 additions and 16 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
|
|
33
app/javascript/mastodon/components/emoji_view.jsx
Normal file
33
app/javascript/mastodon/components/emoji_view.jsx
Normal 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 }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
107
app/javascript/mastodon/features/emoji_reactions/index.jsx
Normal file
107
app/javascript/mastodon/features/emoji_reactions/index.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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')) {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
34
app/serializers/rest/emoji_reaction_account_serializer.rb
Normal file
34
app/serializers/rest/emoji_reaction_account_serializer.rb
Normal 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
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue