Compare commits
9 commits
main
...
wip/reacti
Author | SHA1 | Date | |
---|---|---|---|
|
001d5e24b8 | ||
|
da10e39d33 | ||
|
e1e4b734f6 | ||
|
68aea0d2f5 | ||
|
2ab563e06c | ||
|
0a6eaa16dc | ||
|
31406cb0f2 | ||
|
a4bfa67151 | ||
|
9862e1591e |
21 changed files with 518 additions and 34 deletions
|
@ -1,8 +1,18 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
|
class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
|
REACTIONS_LIMIT = 30
|
||||||
before_action :require_user!
|
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }, only: [:create, :destroy]
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: [:index]
|
||||||
|
before_action :require_user!, only: [:create, :destroy]
|
||||||
|
before_action :set_reactions, only: [:index]
|
||||||
|
after_action :insert_pagination_headers, only: [:index]
|
||||||
|
|
||||||
|
def index
|
||||||
|
cache_if_unauthenticated!
|
||||||
|
render json: @reactions, each_serializer: REST::StatusReactionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ReactService.new.call(current_account, @status, params[:id])
|
ReactService.new.call(current_account, @status, params[:id])
|
||||||
|
@ -16,4 +26,83 @@ class Api::V1::Statuses::ReactionsController < Api::V1::Statuses::BaseController
|
||||||
rescue Mastodon::NotPermittedError
|
rescue Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_reactions
|
||||||
|
@reactions = ordered_reactions.select(
|
||||||
|
[:id, :account_id, :name, :custom_emoji_id].tap do |values|
|
||||||
|
values << value_for_reaction_me_column(current_account)
|
||||||
|
end
|
||||||
|
).to_a_paginated_by_id(
|
||||||
|
limit_param(REACTIONS_LIMIT),
|
||||||
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ordered_reactions
|
||||||
|
filtered_reactions.group(:status_id, :id, :account_id, :name, :custom_emoji_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_reactions
|
||||||
|
initial_reactions = StatusReaction.where(status: @status)
|
||||||
|
if filtered?
|
||||||
|
initial_reactions.where(name: params[:emoji])
|
||||||
|
else
|
||||||
|
initial_reactions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered?
|
||||||
|
params[:emoji].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_for_reaction_me_column(account)
|
||||||
|
if account.nil?
|
||||||
|
'FALSE AS me'
|
||||||
|
else
|
||||||
|
<<~SQL.squish
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM status_reactions inner_reactions
|
||||||
|
WHERE inner_reactions.account_id = #{account.id}
|
||||||
|
AND inner_reactions.status_id = status_reactions.status_id
|
||||||
|
AND inner_reactions.name = status_reactions.name
|
||||||
|
AND (
|
||||||
|
inner_reactions.custom_emoji_id = status_reactions.custom_emoji_id
|
||||||
|
OR inner_reactions.custom_emoji_id IS NULL
|
||||||
|
AND status_reactions.custom_emoji_id IS NULL
|
||||||
|
)
|
||||||
|
) AS me
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_status_reactions_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_status_reactions_url pagination_params(since_id: pagination_since_id) unless @reactions.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@reactions.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@reactions.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@reactions.size == limit_param(REACTIONS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params_slice(:limit, :emoji).merge(core_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,10 @@ export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||||
|
|
||||||
|
export const REACTIONS_EXPAND_REQUEST = 'REACTIONS_EXPAND_REQUEST';
|
||||||
|
export const REACTIONS_EXPAND_SUCCESS = 'REACTIONS_EXPAND_SUCCESS';
|
||||||
|
export const REACTIONS_EXPAND_FAIL = 'REACTIONS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||||
|
@ -23,6 +27,10 @@ export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST';
|
||||||
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS';
|
||||||
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL';
|
||||||
|
|
||||||
|
export const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||||
|
export const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||||
|
export const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||||
|
|
||||||
export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
||||||
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||||
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
||||||
|
@ -287,6 +295,90 @@ export function unbookmarkFail(status, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchReactions(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchReactionsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${id}/reactions`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
const accounts = response.data.map(item => item.account);
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(fetchReactionsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchReactionsFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReactionsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReactionsSuccess(id, reactions, next) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
reactions,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReactionsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReactions(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'reactions', id, 'next']);
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandReactionsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
const accounts = response.data.map(item => item.account);
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(expandReactionsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(accounts.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandReactionsFail(id, error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReactionsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReactionsSuccess(id, reactions, next) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
reactions,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReactionsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: REACTIONS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchReblogs(id) {
|
export function fetchReblogs(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchReblogsRequest(id));
|
dispatch(fetchReblogsRequest(id));
|
||||||
|
|
5
app/javascript/flavours/glitch/api_types/reaction.ts
Normal file
5
app/javascript/flavours/glitch/api_types/reaction.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ApiStatusReactionJSON {
|
||||||
|
name: string;
|
||||||
|
static_url?: string | undefined;
|
||||||
|
url?: string | undefined;
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
|
import { AvatarOverlay } from './avatar_overlay';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { FollowersCounter } from './counters';
|
import { FollowersCounter } from './counters';
|
||||||
import { DisplayName } from './display_name';
|
import { DisplayName } from './display_name';
|
||||||
|
@ -42,6 +43,7 @@ class Account extends ImmutablePureComponent {
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
onMuteNotifications: PropTypes.func,
|
onMuteNotifications: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
overlayEmoji: PropTypes.object,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
minimal: PropTypes.bool,
|
minimal: PropTypes.bool,
|
||||||
defaultAction: PropTypes.string,
|
defaultAction: PropTypes.string,
|
||||||
|
@ -50,6 +52,7 @@ class Account extends ImmutablePureComponent {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
size: 46,
|
size: 46,
|
||||||
|
overlayEmoji: { name: null }
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
|
@ -73,7 +76,7 @@ class Account extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
|
const { account, intl, hidden, withBio, defaultAction, overlayEmoji, size, minimal } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
return <EmptyAccount size={size} minimal={minimal} />;
|
||||||
|
@ -138,12 +141,19 @@ class Account extends ImmutablePureComponent {
|
||||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let statusAvatar;
|
||||||
|
if (!overlayEmoji.name) {
|
||||||
|
statusAvatar = <Avatar account={account} size={size} />;
|
||||||
|
} else {
|
||||||
|
statusAvatar = <AvatarOverlay account={account} emoji={overlayEmoji} baseSize={size} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||||
<div className='account__wrapper'>
|
<div className='account__wrapper'>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||||
<div className='account__avatar-wrapper'>
|
<div className='account__avatar-wrapper'>
|
||||||
<Avatar account={account} size={size} />
|
{statusAvatar}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__contents'>
|
<div className='account__contents'>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
import type { StatusReaction } from 'flavours/glitch/models/reaction';
|
||||||
|
|
||||||
import { useHovering } from '../hooks/useHovering';
|
import { useHovering } from '../hooks/useHovering';
|
||||||
import { autoPlayGif } from '../initial_state';
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
|
import { Emoji } from './status_reactions';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend?: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
|
emoji?: StatusReaction | undefined;
|
||||||
size?: number;
|
size?: number;
|
||||||
baseSize?: number;
|
baseSize?: number;
|
||||||
overlaySize?: number;
|
overlaySize?: number;
|
||||||
|
@ -14,6 +18,7 @@ interface Props {
|
||||||
export const AvatarOverlay: React.FC<Props> = ({
|
export const AvatarOverlay: React.FC<Props> = ({
|
||||||
account,
|
account,
|
||||||
friend,
|
friend,
|
||||||
|
emoji,
|
||||||
size = 46,
|
size = 46,
|
||||||
baseSize = 36,
|
baseSize = 36,
|
||||||
overlaySize = 24,
|
overlaySize = 24,
|
||||||
|
@ -27,6 +32,32 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||||
? friend?.get('avatar')
|
? friend?.get('avatar')
|
||||||
: friend?.get('avatar_static');
|
: friend?.get('avatar_static');
|
||||||
|
|
||||||
|
let overlayElement;
|
||||||
|
if (friendSrc) {
|
||||||
|
overlayElement = (
|
||||||
|
<div
|
||||||
|
className='account__avatar'
|
||||||
|
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
|
||||||
|
data-avatar-of={`@${friend?.get('acct')}`}
|
||||||
|
>
|
||||||
|
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
overlayElement = (
|
||||||
|
<div className='account__emoji' data-emoji-name={emoji?.name}>
|
||||||
|
{emoji && (
|
||||||
|
<Emoji
|
||||||
|
emoji={emoji.name}
|
||||||
|
hovered={hovering}
|
||||||
|
url={emoji.url}
|
||||||
|
staticUrl={emoji.static_url}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='account__avatar-overlay'
|
className='account__avatar-overlay'
|
||||||
|
@ -43,15 +74,7 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||||
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
|
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='account__avatar-overlay-overlay'>
|
<div className='account__avatar-overlay-overlay'>{overlayElement}</div>
|
||||||
<div
|
|
||||||
className='account__avatar'
|
|
||||||
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
|
|
||||||
data-avatar-of={`@${friend?.get('acct')}`}
|
|
||||||
>
|
|
||||||
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,7 +32,7 @@ import StatusContent from './status_content';
|
||||||
import StatusHeader from './status_header';
|
import StatusHeader from './status_header';
|
||||||
import StatusIcons from './status_icons';
|
import StatusIcons from './status_icons';
|
||||||
import StatusPrepend from './status_prepend';
|
import StatusPrepend from './status_prepend';
|
||||||
import StatusReactions from './status_reactions';
|
import { StatusReactions } from './status_reactions';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { assetHost } from '../utils/config';
|
||||||
|
|
||||||
import { AnimatedNumber } from './animated_number';
|
import { AnimatedNumber } from './animated_number';
|
||||||
|
|
||||||
export default class StatusReactions extends ImmutablePureComponent {
|
export class StatusReactions extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
statusId: PropTypes.string.isRequired,
|
statusId: PropTypes.string.isRequired,
|
||||||
|
@ -107,6 +107,7 @@ class Reaction extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type='button'
|
||||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
@ -131,7 +132,7 @@ class Reaction extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Emoji extends React.PureComponent {
|
export class Emoji extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
emoji: PropTypes.string.isRequired,
|
emoji: PropTypes.string.isRequired,
|
||||||
|
|
120
app/javascript/flavours/glitch/features/reactions/index.jsx
Normal file
120
app/javascript/flavours/glitch/features/reactions/index.jsx
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
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 { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import MoodIcon from '@/material-icons/400-24px/mood.svg?react';
|
||||||
|
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
|
import { fetchReactions, expandReactions } from '../../actions/interactions';
|
||||||
|
import ColumnHeader from '../../components/column_header';
|
||||||
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
|
import AccountContainer from '../../containers/account_container';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.reacted_by', defaultMessage: 'Reacted by' },
|
||||||
|
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
reactions: state.getIn(['status_reactions', 'reactions', props.params.statusId, 'items']),
|
||||||
|
hasMore: !!state.getIn(['status_reactions', 'reactions', props.params.statusId, 'next']),
|
||||||
|
isLoading: state.getIn(['status_reactions', 'reactions', props.params.statusId, 'isLoading'], true),
|
||||||
|
});
|
||||||
|
|
||||||
|
class Reactions extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
reactions: ImmutablePropTypes.orderedSet,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount () {
|
||||||
|
if (!this.props.accountIds) {
|
||||||
|
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
};
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRefresh = () => {
|
||||||
|
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandReactions(this.props.params.statusId));
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, reactions, hasMore, isLoading, multiColumn } = this.props;
|
||||||
|
|
||||||
|
if (!reactions) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountIds = reactions.map(v => v.account);
|
||||||
|
const reactionsByAccount = new Map(reactions.map(v => [v.account, v]));
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column ref={this.setRef}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='mood'
|
||||||
|
iconComponent={MoodIcon}
|
||||||
|
title={intl.formatMessage(messages.heading)}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
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' icon={RefreshIcon} /></button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='reactions'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{accountIds.map(id =>
|
||||||
|
<AccountContainer key={id} id={id} withNote={false} overlayEmoji={reactionsByAccount.get(id)} />,
|
||||||
|
)}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(injectIntl(Reactions));
|
|
@ -21,7 +21,7 @@ import { Avatar } from '../../../components/avatar';
|
||||||
import { DisplayName } from '../../../components/display_name';
|
import { DisplayName } from '../../../components/display_name';
|
||||||
import MediaGallery from '../../../components/media_gallery';
|
import MediaGallery from '../../../components/media_gallery';
|
||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from '../../../components/status_content';
|
||||||
import StatusReactions from '../../../components/status_reactions';
|
import { StatusReactions } from '../../../components/status_reactions';
|
||||||
import Audio from '../../audio';
|
import Audio from '../../audio';
|
||||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||||
import Video from '../../video';
|
import Video from '../../video';
|
||||||
|
@ -137,6 +137,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
|
let reactionLink = '';
|
||||||
|
|
||||||
// Depending on user settings, some media are considered as parts of the
|
// Depending on user settings, some media are considered as parts of the
|
||||||
// contents (affected by CW) while other will be displayed outside of the
|
// contents (affected by CW) while other will be displayed outside of the
|
||||||
|
@ -275,6 +276,14 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
reactionLink = (
|
||||||
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reactions`} className='detailed-status__link'>
|
||||||
|
<span className='detailed-status__reactions'>
|
||||||
|
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage id='status.reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0) }} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
favouriteLink = (
|
favouriteLink = (
|
||||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
|
@ -284,6 +293,14 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
reactionLink = (
|
||||||
|
<a href={`/interact/${status.get('id')}?type=reaction`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
|
<span className='detailed-status__reactions'>
|
||||||
|
<AnimatedNumber value={status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0)} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage id='status.reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('reactions').reduce((total, obj) => total + obj.get('count'), 0) }} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
|
@ -339,6 +356,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
{reblogLink}
|
{reblogLink}
|
||||||
{reblogLink && <>·</>}
|
{reblogLink && <>·</>}
|
||||||
{favouriteLink}
|
{favouriteLink}
|
||||||
|
·
|
||||||
|
{reactionLink}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -45,6 +45,7 @@ import {
|
||||||
HomeTimeline,
|
HomeTimeline,
|
||||||
Followers,
|
Followers,
|
||||||
Following,
|
Following,
|
||||||
|
Reactions,
|
||||||
Reblogs,
|
Reblogs,
|
||||||
Favourites,
|
Favourites,
|
||||||
DirectTimeline,
|
DirectTimeline,
|
||||||
|
@ -234,6 +235,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
<WrappedRoute path={['/accounts/:id/following', '/users/:acct/following', '/@:acct/following']} component={Following} content={children} />
|
||||||
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
<WrappedRoute path={['/@:acct/media', '/accounts/:id/media']} component={AccountGallery} content={children} />
|
||||||
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
<WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
|
||||||
|
<WrappedRoute path='/@:acct/:statusId/reactions' component={Reactions} 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} />
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,10 @@ export function Favourites () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'../../favourites');
|
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'../../favourites');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Reactions () {
|
||||||
|
return import(/* webpackChunkName: "flavours/glitch/async/reactions" */'../../reactions');
|
||||||
|
}
|
||||||
|
|
||||||
export function FollowRequests () {
|
export function FollowRequests () {
|
||||||
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'../../follow_requests');
|
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'../../follow_requests');
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"boost_modal.missing_description": "This toot contains some media without description",
|
"boost_modal.missing_description": "This toot contains some media without description",
|
||||||
"column.favourited_by": "Favourited by",
|
"column.favourited_by": "Favourited by",
|
||||||
"column.heading": "Misc",
|
"column.heading": "Misc",
|
||||||
|
"column.reacted_by": "Reacted by",
|
||||||
"column.reblogged_by": "Boosted by",
|
"column.reblogged_by": "Boosted by",
|
||||||
"column.subheading": "Miscellaneous options",
|
"column.subheading": "Miscellaneous options",
|
||||||
"column_header.profile": "Profile",
|
"column_header.profile": "Profile",
|
||||||
|
@ -162,6 +163,8 @@
|
||||||
"status.is_poll": "This toot is a poll",
|
"status.is_poll": "This toot is a poll",
|
||||||
"status.local_only": "Only visible from your instance",
|
"status.local_only": "Only visible from your instance",
|
||||||
"status.react": "React",
|
"status.react": "React",
|
||||||
|
"status.reactions": "{count, plural, one {reaction} other {reactions}}",
|
||||||
|
"status.reactions.empty": "No one has reacted to this post yet. When someone does, they will show up here.",
|
||||||
"status.uncollapse": "Uncollapse",
|
"status.uncollapse": "Uncollapse",
|
||||||
"suggestions.dismiss": "Dismiss suggestion",
|
"suggestions.dismiss": "Dismiss suggestion",
|
||||||
"tenor.error": "Oops! Something went wrong. Please, try again.",
|
"tenor.error": "Oops! Something went wrong. Please, try again.",
|
||||||
|
|
13
app/javascript/flavours/glitch/models/reaction.ts
Normal file
13
app/javascript/flavours/glitch/models/reaction.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
import { Record } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiStatusReactionJSON } from 'flavours/glitch/api_types/reaction';
|
||||||
|
|
||||||
|
type StatusReactionShape = Required<ApiStatusReactionJSON>;
|
||||||
|
export type StatusReaction = RecordOf<StatusReactionShape>;
|
||||||
|
|
||||||
|
export const CustomEmojiFactory = Record<StatusReactionShape>({
|
||||||
|
name: '',
|
||||||
|
static_url: '',
|
||||||
|
url: '',
|
||||||
|
});
|
|
@ -38,6 +38,7 @@ import search from './search';
|
||||||
import server from './server';
|
import server from './server';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
|
import status_reactions from './status_reactions';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import suggestions from './suggestions';
|
import suggestions from './suggestions';
|
||||||
import tags from './tags';
|
import tags from './tags';
|
||||||
|
@ -86,6 +87,7 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
tags,
|
tags,
|
||||||
followed_tags,
|
followed_tags,
|
||||||
|
status_reactions,
|
||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
};
|
};
|
||||||
|
|
57
app/javascript/flavours/glitch/reducers/status_reactions.js
Normal file
57
app/javascript/flavours/glitch/reducers/status_reactions.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
REACTIONS_FETCH_SUCCESS,
|
||||||
|
REACTIONS_EXPAND_SUCCESS,
|
||||||
|
REACTIONS_FETCH_REQUEST,
|
||||||
|
REACTIONS_EXPAND_REQUEST,
|
||||||
|
REACTIONS_FETCH_FAIL,
|
||||||
|
REACTIONS_EXPAND_FAIL,
|
||||||
|
} from '../actions/interactions';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
reactions: ImmutableMap({
|
||||||
|
next: null,
|
||||||
|
isLoading: false,
|
||||||
|
items: ImmutableOrderedSet(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeList = (state, path, reactions, next) => {
|
||||||
|
const filteredReactions = reactions.map(v => {
|
||||||
|
v.account = v.account.id;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
return state.setIn(path, ImmutableMap({
|
||||||
|
next,
|
||||||
|
items: ImmutableOrderedSet(filteredReactions),
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendToList = (state, path, reactions, next) => {
|
||||||
|
const filteredReactions = reactions.map(v => {
|
||||||
|
v.account = v.account.id;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
return state.updateIn(path, map => {
|
||||||
|
return map.set('next', next).set('isLoading', false).update('items', list => list.concat(filteredReactions));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function statusReactions(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case REACTIONS_FETCH_SUCCESS:
|
||||||
|
return normalizeList(state, ['reactions', action.id], action.reactions, action.next);
|
||||||
|
case REACTIONS_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, ['reactions', action.id], action.reactions, action.next);
|
||||||
|
case REACTIONS_FETCH_REQUEST:
|
||||||
|
case REACTIONS_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['reactions', action.id, 'isLoading'], true);
|
||||||
|
case REACTIONS_FETCH_FAIL:
|
||||||
|
case REACTIONS_EXPAND_FAIL:
|
||||||
|
return state.setIn(['reactions', action.id, 'isLoading'], false);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ class StatusCacheHydrator
|
||||||
reactions = @status.reactions(account_id)
|
reactions = @status.reactions(account_id)
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
reactions,
|
reactions,
|
||||||
each_serializer: REST::ReactionSerializer,
|
each_serializer: REST::StatusReactionSerializer,
|
||||||
scope: account_id, # terrible
|
scope: account_id, # terrible
|
||||||
scope_name: :current_user
|
scope_name: :current_user
|
||||||
).as_json
|
).as_json
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
class StatusReaction < ApplicationRecord
|
class StatusReaction < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :status, inverse_of: :status_reactions
|
belongs_to :status, inverse_of: :status_reactions
|
||||||
belongs_to :custom_emoji, optional: true
|
belongs_to :custom_emoji, optional: true
|
||||||
|
|
|
@ -21,14 +21,6 @@ class REST::ReactionSerializer < ActiveModel::Serializer
|
||||||
object.custom_emoji.present?
|
object.custom_emoji.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def name
|
|
||||||
if extern?
|
|
||||||
[object.name, '@', object.custom_emoji.domain].join
|
|
||||||
else
|
|
||||||
object.name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def url
|
def url
|
||||||
full_asset_url(object.custom_emoji.image.url)
|
full_asset_url(object.custom_emoji.image.url)
|
||||||
end
|
end
|
||||||
|
@ -36,10 +28,4 @@ class REST::ReactionSerializer < ActiveModel::Serializer
|
||||||
def static_url
|
def static_url
|
||||||
full_asset_url(object.custom_emoji.image.url(:static))
|
full_asset_url(object.custom_emoji.image.url(:static))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def extern?
|
|
||||||
custom_emoji? && object.custom_emoji.domain.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
55
app/serializers/rest/status_reaction_serializer.rb
Normal file
55
app/serializers/rest/status_reaction_serializer.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::StatusReactionSerializer < ActiveModel::Serializer
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
attributes :name
|
||||||
|
|
||||||
|
attribute :id, unless: :no_id?
|
||||||
|
attribute :me, if: :current_user? # are this and count worth keeping?
|
||||||
|
attribute :url, if: :custom_emoji?
|
||||||
|
attribute :static_url, if: :custom_emoji?
|
||||||
|
attribute :count, if: :respond_to_count?
|
||||||
|
|
||||||
|
belongs_to :account, serializer: REST::AccountSerializer, unless: :respond_to_count?
|
||||||
|
|
||||||
|
delegate :count, to: :object
|
||||||
|
|
||||||
|
def respond_to_count?
|
||||||
|
object.respond_to?(:count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_id?
|
||||||
|
object.id.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user?
|
||||||
|
!current_user.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_emoji?
|
||||||
|
object.custom_emoji.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def name
|
||||||
|
if extern?
|
||||||
|
[object.name, '@', object.custom_emoji.domain].join
|
||||||
|
else
|
||||||
|
object.name
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def extern?
|
||||||
|
custom_emoji? && object.custom_emoji.domain.present?
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,7 +28,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||||
has_many :ordered_mentions, key: :mentions
|
has_many :ordered_mentions, key: :mentions
|
||||||
has_many :tags
|
has_many :tags
|
||||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||||
has_many :reactions, serializer: REST::ReactionSerializer
|
has_many :reactions, serializer: REST::StatusReactionSerializer
|
||||||
|
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace :api, format: false 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 :reactions, controller: :reactions, only: :index
|
||||||
resource :reblog, only: :create
|
resource :reblog, only: :create
|
||||||
post :unreblog, to: 'reblogs#destroy'
|
post :unreblog, to: 'reblogs#destroy'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue