Add status references list page
This commit is contained in:
parent
9d9a85c770
commit
c656f41b35
10 changed files with 239 additions and 5 deletions
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::ReferredByStatusesController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||||
|
before_action :set_status
|
||||||
|
after_action :insert_pagination_headers
|
||||||
|
|
||||||
|
def index
|
||||||
|
@statuses = load_statuses
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_statuses
|
||||||
|
cached_references
|
||||||
|
end
|
||||||
|
|
||||||
|
def cached_references
|
||||||
|
cache_collection(results, Status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def results
|
||||||
|
@results ||= Status.where(id: @status.referenced_by_status_objects.select(:status_id)).paginate_by_max_id(
|
||||||
|
limit_param(DEFAULT_STATUSES_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_referred_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_status_referred_by_index_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
results.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
results.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
results.size == limit_param(DEFAULT_STATUSES_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
|
|
@ -1,6 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
|
@ -34,6 +34,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 STATUS_REFERENCES_FETCH_REQUEST = 'STATUS_REFERENCES_FETCH_REQUEST';
|
||||||
|
export const STATUS_REFERENCES_FETCH_SUCCESS = 'STATUS_REFERENCES_FETCH_SUCCESS';
|
||||||
|
export const STATUS_REFERENCES_FETCH_FAIL = 'STATUS_REFERENCES_FETCH_FAIL';
|
||||||
|
|
||||||
export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST';
|
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_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
|
||||||
export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL';
|
export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL';
|
||||||
|
@ -470,6 +474,41 @@ export function fetchEmojiReactionsFail(id, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchStatusReferences(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchStatusReferencesRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${id}/referred_by`).then(response => {
|
||||||
|
dispatch(importFetchedStatuses(response.data));
|
||||||
|
dispatch(fetchStatusReferencesSuccess(id, response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchStatusReferencesFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStatusReferencesRequest(id) {
|
||||||
|
return {
|
||||||
|
type: STATUS_REFERENCES_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStatusReferencesSuccess(id, statuses) {
|
||||||
|
return {
|
||||||
|
type: STATUS_REFERENCES_FETCH_SUCCESS,
|
||||||
|
id,
|
||||||
|
statuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStatusReferencesFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: STATUS_REFERENCES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function pin(status) {
|
export function pin(status) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(pinRequest(status));
|
dispatch(pinRequest(status));
|
||||||
|
|
|
@ -96,7 +96,7 @@ class FilterBar extends PureComponent {
|
||||||
onClick={this.onClick('status_reference')}
|
onClick={this.onClick('status_reference')}
|
||||||
title={intl.formatMessage(tooltips.status_references)}
|
title={intl.formatMessage(tooltips.status_references)}
|
||||||
>
|
>
|
||||||
<Icon id='retweet' fixedWidth />
|
<Icon id='link' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'poll' ? 'active' : ''}
|
className={selectedFilter === 'poll' ? 'active' : ''}
|
||||||
|
|
|
@ -294,10 +294,10 @@ class Notification extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
<div className={classNames('notification notification-status_reference focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.statusReference, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='retweet' fixedWidth />
|
<Icon id='link' fixedWidth />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span title={notification.get('created_at')}>
|
<span title={notification.get('created_at')}>
|
||||||
|
|
|
@ -137,6 +137,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
let emojiReactionsLink = '';
|
let emojiReactionsLink = '';
|
||||||
|
let statusReferencesLink = '';
|
||||||
let edited = '';
|
let edited = '';
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
|
@ -310,6 +311,26 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.context.router) {
|
||||||
|
statusReferencesLink = (
|
||||||
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/references`} className='detailed-status__link'>
|
||||||
|
<Icon id='link' />
|
||||||
|
<span className='detailed-status__favorites'>
|
||||||
|
<AnimatedNumber value={status.get('status_referred_by_count')} />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
statusReferencesLink = (
|
||||||
|
<a href={`/interact/${status.get('id')}?type=references`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
|
<Icon id='link' />
|
||||||
|
<span className='detailed-status__favorites'>
|
||||||
|
<AnimatedNumber value={status.get('status_referred_by_count')} />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.get('edited_at')) {
|
if (status.get('edited_at')) {
|
||||||
edited = (
|
edited = (
|
||||||
<>
|
<>
|
||||||
|
@ -347,7 +368,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
</a>{edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink}
|
</a>{edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink} - {statusReferencesLink}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
95
app/javascript/mastodon/features/status_references/index.jsx
Normal file
95
app/javascript/mastodon/features/status_references/index.jsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
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 { fetchStatusReferences } from 'mastodon/actions/interactions';
|
||||||
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
accountIds: state.getIn(['user_lists', 'referred_by', props.params.statusId]),
|
||||||
|
});
|
||||||
|
|
||||||
|
class StatusReferences extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
params: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
UNSAFE_componentWillMount () {
|
||||||
|
if (!this.props.accountIds) {
|
||||||
|
this.props.dispatch(fetchStatusReferences(this.props.params.statusId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||||
|
this.props.dispatch(fetchStatusReferences(nextProps.params.statusId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRefresh = () => {
|
||||||
|
this.props.dispatch(fetchStatusReferences(this.props.params.statusId));
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, accountIds, multiColumn } = this.props;
|
||||||
|
|
||||||
|
if (!accountIds) {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.status_references' defaultMessage='No one has referred 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='references'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
>
|
||||||
|
{accountIds.map(id =>
|
||||||
|
<StatusContainer key={id} id={id} withNote={false} />,
|
||||||
|
)}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(injectIntl(StatusReferences));
|
|
@ -46,6 +46,7 @@ import {
|
||||||
Reblogs,
|
Reblogs,
|
||||||
Favourites,
|
Favourites,
|
||||||
EmojiReactions,
|
EmojiReactions,
|
||||||
|
StatusReferences,
|
||||||
DirectTimeline,
|
DirectTimeline,
|
||||||
HashtagTimeline,
|
HashtagTimeline,
|
||||||
Notifications,
|
Notifications,
|
||||||
|
@ -221,6 +222,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<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} />
|
<WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
|
||||||
|
<WrappedRoute path='/@:acct/:statusId/references' component={StatusReferences} 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} />
|
||||||
|
@ -229,6 +231,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<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='/statuses/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
|
||||||
|
<WrappedRoute path='/statuses/:statusId/references' component={StatusReferences} 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} />
|
||||||
|
|
|
@ -82,6 +82,10 @@ export function EmojiReactions () {
|
||||||
return import(/* webpackChunkName: "features/emoji_reactions" */'../../emoji_reactions');
|
return import(/* webpackChunkName: "features/emoji_reactions" */'../../emoji_reactions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StatusReferences () {
|
||||||
|
return import(/* webpackChunkName: "features/status_references" */'../../status_references');
|
||||||
|
}
|
||||||
|
|
||||||
export function FollowRequests () {
|
export function FollowRequests () {
|
||||||
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
REBLOGS_FETCH_SUCCESS,
|
REBLOGS_FETCH_SUCCESS,
|
||||||
FAVOURITES_FETCH_SUCCESS,
|
FAVOURITES_FETCH_SUCCESS,
|
||||||
EMOJI_REACTIONS_FETCH_SUCCESS,
|
EMOJI_REACTIONS_FETCH_SUCCESS,
|
||||||
|
STATUS_REFERENCES_FETCH_SUCCESS,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import {
|
import {
|
||||||
MUTES_FETCH_REQUEST,
|
MUTES_FETCH_REQUEST,
|
||||||
|
@ -75,6 +76,7 @@ const initialState = ImmutableMap({
|
||||||
reblogged_by: initialListState,
|
reblogged_by: initialListState,
|
||||||
favourited_by: initialListState,
|
favourited_by: initialListState,
|
||||||
emoji_reactioned_by: initialListState,
|
emoji_reactioned_by: initialListState,
|
||||||
|
referred_by: initialListState,
|
||||||
follow_requests: initialListState,
|
follow_requests: initialListState,
|
||||||
blocks: initialListState,
|
blocks: initialListState,
|
||||||
mutes: initialListState,
|
mutes: initialListState,
|
||||||
|
@ -141,6 +143,8 @@ export default function userLists(state = initialState, action) {
|
||||||
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:
|
case EMOJI_REACTIONS_FETCH_SUCCESS:
|
||||||
return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts));
|
return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts));
|
||||||
|
case STATUS_REFERENCES_FETCH_SUCCESS:
|
||||||
|
return state.setIn(['referred_by', action.id], ImmutableList(action.statuses.map(item => item.id)));
|
||||||
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:
|
||||||
|
|
|
@ -12,6 +12,7 @@ namespace :api, format: false do
|
||||||
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
|
resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index
|
||||||
resources :emoji_reactioned_by_slim, controller: :emoji_reactioned_by_accounts_slim, only: :index
|
resources :emoji_reactioned_by_slim, controller: :emoji_reactioned_by_accounts_slim, only: :index
|
||||||
|
resources :referred_by, controller: :referred_by_statuses, 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