Merge commit 'eaa1f9e450
' into kb_migration
This commit is contained in:
commit
2a813d517d
73 changed files with 987 additions and 72 deletions
|
@ -12,7 +12,7 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||
authorize @report, :show?
|
||||
|
||||
case action_from_button
|
||||
when 'delete', 'mark_as_sensitive'
|
||||
when 'delete', 'mark_as_sensitive', 'force_cw'
|
||||
status_batch_action = Admin::StatusBatchAction.new(
|
||||
type: action_from_button,
|
||||
status_ids: @report.status_ids,
|
||||
|
@ -52,6 +52,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||
'delete'
|
||||
elsif params[:mark_as_sensitive]
|
||||
'mark_as_sensitive'
|
||||
elsif params[:force_cw]
|
||||
'force_cw'
|
||||
elsif params[:silence]
|
||||
'silence'
|
||||
elsif params[:suspend]
|
||||
|
|
|
@ -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), visibility: [:public, :public_unlisted, :unlisted, :login]).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
|
|
@ -44,11 +44,13 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
|
||||
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account)
|
||||
descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit)
|
||||
references_results = @status.references
|
||||
loaded_ancestors = cache_collection(ancestors_results, Status)
|
||||
loaded_descendants = cache_collection(descendants_results, Status)
|
||||
loaded_references = cache_collection(references_results, Status)
|
||||
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants
|
||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants, references: loaded_references)
|
||||
statuses = [@status] + @context.ancestors + @context.descendants + @context.references
|
||||
|
||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||
end
|
||||
|
|
|
@ -58,6 +58,10 @@ module FormattingHelper
|
|||
end
|
||||
|
||||
def account_field_value_format(field, with_rel_me: true)
|
||||
html_aware_format(field.value, field.account.local?, markdown: false, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||
if field.verified? && !field.account.local?
|
||||
TextFormatter.shortened_link(field.value_for_verification)
|
||||
else
|
||||
html_aware_format(field.value, field.account.local?, markdown: false, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,6 +62,7 @@ export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
|||
|
||||
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
||||
export const COMPOSE_EXPIRATION_INSERT = 'COMPOSE_EXPIRATION_INSERT';
|
||||
export const COMPOSE_REFERENCE_INSERT = 'COMPOSE_REFERENCE_INSERT';
|
||||
|
||||
export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
|
||||
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||
|
@ -770,6 +771,14 @@ export function insertExpirationCompose(position, data) {
|
|||
};
|
||||
}
|
||||
|
||||
export function insertReferenceCompose(position, url) {
|
||||
return {
|
||||
type: COMPOSE_REFERENCE_INSERT,
|
||||
position,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
export function changeComposing(value) {
|
||||
return {
|
||||
type: COMPOSE_COMPOSING_CHANGE,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import api from '../api';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
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_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_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
|
||||
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) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(pinRequest(status));
|
||||
|
|
|
@ -143,6 +143,7 @@ const excludeTypesFromFilter = filter => {
|
|||
'favourite',
|
||||
'emoji_reaction',
|
||||
'reblog',
|
||||
'status_reference',
|
||||
'mention',
|
||||
'poll',
|
||||
'status',
|
||||
|
|
|
@ -181,8 +181,8 @@ export function fetchContext(id) {
|
|||
dispatch(fetchContextRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
|
||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
||||
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants).concat(response.data.references)));
|
||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants, response.data.references));
|
||||
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.status === 404) {
|
||||
|
@ -201,12 +201,13 @@ export function fetchContextRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchContextSuccess(id, ancestors, descendants) {
|
||||
export function fetchContextSuccess(id, ancestors, descendants, references) {
|
||||
return {
|
||||
type: CONTEXT_FETCH_SUCCESS,
|
||||
id,
|
||||
ancestors,
|
||||
descendants,
|
||||
references,
|
||||
statuses: ancestors.concat(descendants),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ const messages = defineMessages({
|
|||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
|
||||
reference: { id: 'status.reference', defaultMessage: 'Add reference' },
|
||||
hide: { id: 'status.hide', defaultMessage: 'Hide post' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
|
@ -251,6 +252,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
navigator.clipboard.writeText(url);
|
||||
};
|
||||
|
||||
handleReference = () => {
|
||||
this.props.onReference(this.props.status);
|
||||
};
|
||||
|
||||
handleHideClick = () => {
|
||||
this.props.onFilter();
|
||||
};
|
||||
|
@ -289,6 +294,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
menu.push(null);
|
||||
|
||||
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancelReblog : messages.reblog), action: this.handleReblogForceModalClick });
|
||||
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
|
||||
if (writtenByMe && pinnableStatus) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
insertReferenceCompose,
|
||||
} from '../actions/compose';
|
||||
import {
|
||||
blockDomain,
|
||||
|
@ -192,6 +193,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
});
|
||||
},
|
||||
|
||||
onReference (status) {
|
||||
dispatch(insertReferenceCompose(0, status.get('url')));
|
||||
},
|
||||
|
||||
onTranslate (status) {
|
||||
if (status.get('translation')) {
|
||||
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||
|
|
|
@ -165,8 +165,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
},
|
||||
|
||||
onAddToAntenna (account) {
|
||||
dispatch(openModal('ANTENNA_ADDER', {
|
||||
accountId: account.get('id'),
|
||||
dispatch(openModal({
|
||||
modalType: 'ANTENNA_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.get('id'),
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
|
|
|
@ -152,6 +152,17 @@ export default class ColumnSettings extends PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-status_reference'>
|
||||
<span id='notifications-status_reference' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status_reference' defaultMessage='References:' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status_reference']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'status_reference']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'status_reference']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status_reference']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-poll'>
|
||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ const tooltips = defineMessages({
|
|||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
|
||||
emojiReactions: { id: 'notifications.filter.emoji_reactions', defaultMessage: 'Stamps' },
|
||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||
status_references: { id: 'notifications.filter.status_references', defaultMessage: 'Status references' },
|
||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
|
@ -90,6 +91,13 @@ class FilterBar extends PureComponent {
|
|||
>
|
||||
<Icon id='retweet' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'status_reference' ? 'active' : ''}
|
||||
onClick={this.onClick('status_reference')}
|
||||
title={intl.formatMessage(tooltips.status_references)}
|
||||
>
|
||||
<Icon id='link' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'poll' ? 'active' : ''}
|
||||
onClick={this.onClick('poll')}
|
||||
|
|
|
@ -28,6 +28,7 @@ const messages = defineMessages({
|
|||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||
statusReference: { id: 'notification.status_reference', defaultMessage: '{name} refered' },
|
||||
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
||||
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
||||
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
|
||||
|
@ -288,6 +289,40 @@ class Notification extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderStatusReference (notification, link) {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<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__favourite-icon-wrapper'>
|
||||
<Icon id='link' fixedWidth />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.status_reference' defaultMessage='{name} referenced your status' values={{ name: link }} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
contextType='notifications'
|
||||
getScrollPosition={this.props.getScrollPosition}
|
||||
updateScrollBottom={this.props.updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
unread={this.props.unread}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatus (notification, link) {
|
||||
const { intl, unread, status } = this.props;
|
||||
|
||||
|
@ -479,6 +514,8 @@ class Notification extends ImmutablePureComponent {
|
|||
return this.renderEmojiReaction(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'status_reference':
|
||||
return this.renderStatusReference(notification, link);
|
||||
case 'status':
|
||||
return this.renderStatus(notification, link);
|
||||
case 'update':
|
||||
|
|
|
@ -42,6 +42,7 @@ const messages = defineMessages({
|
|||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
|
||||
reference: { id: 'status.reference', defaultMessage: 'Add reference' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
|
@ -69,6 +70,7 @@ class ActionBar extends PureComponent {
|
|||
onReblogForceModal: PropTypes.func.isRequired,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onEmojiReact: PropTypes.func.isRequired,
|
||||
onReference: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
|
@ -190,6 +192,10 @@ class ActionBar extends PureComponent {
|
|||
navigator.clipboard.writeText(url);
|
||||
};
|
||||
|
||||
handleReference = () => {
|
||||
this.props.onReference(this.props.status);
|
||||
};
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
this.props.onEmojiReact(this.props.status, data);
|
||||
};
|
||||
|
@ -227,6 +233,11 @@ class ActionBar extends PureComponent {
|
|||
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.reblog), action: this.handleReblogForceModalClick });
|
||||
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
let reblogIcon = 'retweet';
|
||||
let favouriteLink = '';
|
||||
let emojiReactionsLink = '';
|
||||
let statusReferencesLink = '';
|
||||
let edited = '';
|
||||
|
||||
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')) {
|
||||
edited = (
|
||||
<>
|
||||
|
@ -347,7 +368,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
<div className='detailed-status__meta'>
|
||||
<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' />
|
||||
</a>{edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink}
|
||||
</a>{edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink} - {statusReferencesLink}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
insertReferenceCompose,
|
||||
} from '../../actions/compose';
|
||||
import {
|
||||
blockDomain,
|
||||
|
@ -88,6 +89,12 @@ const makeMapStateToProps = () => {
|
|||
const getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
|
||||
const getReferenceIds = createSelector([
|
||||
(state, { id }) => state.getIn(['contexts', 'references', id]),
|
||||
], (references) => {
|
||||
return references;
|
||||
});
|
||||
|
||||
const getAncestorsIds = createSelector([
|
||||
(_, { id }) => id,
|
||||
state => state.getIn(['contexts', 'inReplyTos']),
|
||||
|
@ -147,10 +154,12 @@ const makeMapStateToProps = () => {
|
|||
|
||||
let ancestorsIds = Immutable.List();
|
||||
let descendantsIds = Immutable.List();
|
||||
let referenceIds = Immutable.List();
|
||||
|
||||
if (status) {
|
||||
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
|
||||
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
|
||||
referenceIds = getReferenceIds(state, { id: status.get('id') });
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -158,6 +167,7 @@ const makeMapStateToProps = () => {
|
|||
status,
|
||||
ancestorsIds,
|
||||
descendantsIds,
|
||||
referenceIds,
|
||||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
|
||||
|
@ -200,6 +210,7 @@ class Status extends ImmutablePureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
ancestorsIds: ImmutablePropTypes.list,
|
||||
descendantsIds: ImmutablePropTypes.list,
|
||||
referenceIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
askReplyConfirmation: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -356,6 +367,10 @@ class Status extends ImmutablePureComponent {
|
|||
this.handleReblogClick(status, e, true);
|
||||
};
|
||||
|
||||
handleReference = (status) => {
|
||||
this.props.dispatch(insertReferenceCompose(0, status.get('url')));
|
||||
};
|
||||
|
||||
handleBookmarkClick = (status) => {
|
||||
if (status.get('bookmarked')) {
|
||||
this.props.dispatch(unbookmark(status));
|
||||
|
@ -442,8 +457,8 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleToggleAll = () => {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
|
||||
const { status, ancestorsIds, descendantsIds, referenceIds } = this.props;
|
||||
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS(), referenceIds.toJS());
|
||||
|
||||
if (status.get('hidden')) {
|
||||
this.props.dispatch(revealStatus(statusIds));
|
||||
|
@ -636,8 +651,8 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
let ancestors, descendants, references;
|
||||
const { isLoading, status, ancestorsIds, descendantsIds, referenceIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -654,6 +669,10 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (referenceIds && referenceIds.size > 0) {
|
||||
references = <>{this.renderChildren(referenceIds, true)}</>;
|
||||
}
|
||||
|
||||
if (ancestorsIds && ancestorsIds.size > 0) {
|
||||
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
|
||||
}
|
||||
|
@ -690,6 +709,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||
{references}
|
||||
{ancestors}
|
||||
|
||||
<HotKeys handlers={handlers}>
|
||||
|
@ -717,6 +737,7 @@ class Status extends ImmutablePureComponent {
|
|||
onEmojiReact={this.handleEmojiReact}
|
||||
onReblog={this.handleReblogClick}
|
||||
onReblogForceModal={this.handleReblogForceModalClick}
|
||||
onReference={this.handleReference}
|
||||
onBookmark={this.handleBookmarkClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onEdit={this.handleEditClick}
|
||||
|
|
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));
|
|
@ -45,6 +45,7 @@ import {
|
|||
Reblogs,
|
||||
Favourites,
|
||||
EmojiReactions,
|
||||
StatusReferences,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
Notifications,
|
||||
|
@ -223,6 +224,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} 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/references' component={StatusReferences} content={children} />
|
||||
|
||||
{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
|
||||
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
|
||||
|
@ -231,6 +233,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} 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/references' component={StatusReferences} content={children} />
|
||||
|
||||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||
<WrappedRoute path='/blocks' component={Blocks} content={children} />
|
||||
|
|
|
@ -86,6 +86,10 @@ export function EmojiReactions () {
|
|||
return import(/* webpackChunkName: "features/emoji_reactions" */'../../emoji_reactions');
|
||||
}
|
||||
|
||||
export function StatusReferences () {
|
||||
return import(/* webpackChunkName: "features/status_references" */'../../status_references');
|
||||
}
|
||||
|
||||
export function FollowRequests () {
|
||||
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
||||
}
|
||||
|
|
|
@ -427,6 +427,7 @@
|
|||
"notification.poll": "A poll you have voted in has ended",
|
||||
"notification.reblog": "{name} boosted your post",
|
||||
"notification.status": "{name} just posted",
|
||||
"notification.status_reference": "{name} refered your post",
|
||||
"notification.update": "{name} edited a post",
|
||||
"notifications.clear": "Clear notifications",
|
||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||
|
|
|
@ -415,6 +415,7 @@
|
|||
"notification.poll": "アンケートが終了しました",
|
||||
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||
"notification.status": "{name}さんが投稿しました",
|
||||
"notification.status_reference": "{name}さんがあなたの投稿を参照しました",
|
||||
"notification.update": "{name}さんが投稿を編集しました",
|
||||
"notifications.clear": "通知を消去",
|
||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
COMPOSE_COMPOSING_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_EXPIRATION_INSERT,
|
||||
COMPOSE_REFERENCE_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
COMPOSE_UPLOAD_CHANGE_FAIL,
|
||||
|
@ -238,6 +239,41 @@ const insertExpiration = (state, position, data) => {
|
|||
});
|
||||
};
|
||||
|
||||
const insertReference = (state, url) => {
|
||||
const oldText = state.get('text');
|
||||
|
||||
if (oldText.indexOf(`BT ${url}`) >= 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let newLine = '\n\n';
|
||||
if (oldText.length === 0) newLine = '';
|
||||
else if (oldText[oldText.length - 1] === '\n') {
|
||||
if (oldText.length === 1 || oldText[oldText.length - 2] === '\n') {
|
||||
newLine = '';
|
||||
} else {
|
||||
newLine = '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (oldText.length > 0) {
|
||||
const lastLine = oldText.slice(oldText.lastIndexOf('\n') + 1, oldText.length - 1);
|
||||
if (lastLine.startsWith('BT ')) {
|
||||
newLine = '\n';
|
||||
}
|
||||
}
|
||||
|
||||
const referenceText = `${newLine}BT ${url}`;
|
||||
const text = `${oldText}${referenceText}`;
|
||||
|
||||
return state.merge({
|
||||
text,
|
||||
focusDate: new Date(),
|
||||
caretPosition: text.length - referenceText.length,
|
||||
idempotencyKey: uuid(),
|
||||
});
|
||||
};
|
||||
|
||||
const privacyPreference = (a, b) => {
|
||||
const order = ['public', 'public_unlisted', 'unlisted', 'login', 'private', 'direct'];
|
||||
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
|
||||
|
@ -476,6 +512,8 @@ export default function compose(state = initialState, action) {
|
|||
return insertEmoji(state, action.position, action.emoji, action.needsSpace);
|
||||
case COMPOSE_EXPIRATION_INSERT:
|
||||
return insertExpiration(state, action.position, action.data);
|
||||
case COMPOSE_REFERENCE_INSERT:
|
||||
return insertReference(state, action.url);
|
||||
case COMPOSE_UPLOAD_CHANGE_SUCCESS:
|
||||
return state
|
||||
.set('is_changing_upload', false)
|
||||
|
|
|
@ -11,33 +11,38 @@ import { compareId } from '../compare_id';
|
|||
const initialState = ImmutableMap({
|
||||
inReplyTos: ImmutableMap(),
|
||||
replies: ImmutableMap(),
|
||||
references: ImmutableMap(),
|
||||
});
|
||||
|
||||
const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => {
|
||||
const normalizeContext = (immutableState, id, ancestors, descendants, references) => immutableState.withMutations(state => {
|
||||
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
|
||||
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
|
||||
function addReply({ id, in_reply_to_id }) {
|
||||
if (in_reply_to_id && !inReplyTos.has(id)) {
|
||||
state.update('references', immutableReferences => immutableReferences.withMutations(referencePosts => {
|
||||
function addReply({ id, in_reply_to_id }) {
|
||||
if (in_reply_to_id && !inReplyTos.has(id)) {
|
||||
|
||||
replies.update(in_reply_to_id, ImmutableList(), siblings => {
|
||||
const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
|
||||
return siblings.insert(index + 1, id);
|
||||
});
|
||||
replies.update(in_reply_to_id, ImmutableList(), siblings => {
|
||||
const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
|
||||
return siblings.insert(index + 1, id);
|
||||
});
|
||||
|
||||
inReplyTos.set(id, in_reply_to_id);
|
||||
inReplyTos.set(id, in_reply_to_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We know in_reply_to_id of statuses but `id` itself.
|
||||
// So we assume that the status of the id replies to last ancestors.
|
||||
// We know in_reply_to_id of statuses but `id` itself.
|
||||
// So we assume that the status of the id replies to last ancestors.
|
||||
|
||||
ancestors.forEach(addReply);
|
||||
ancestors.forEach(addReply);
|
||||
|
||||
if (ancestors[0]) {
|
||||
addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
|
||||
}
|
||||
if (ancestors[0]) {
|
||||
addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
|
||||
}
|
||||
|
||||
descendants.forEach(addReply);
|
||||
descendants.forEach(addReply);
|
||||
|
||||
referencePosts.set(id, ImmutableList(references.map((r) => r.id)));
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
});
|
||||
|
@ -96,7 +101,7 @@ export default function replies(state = initialState, action) {
|
|||
case ACCOUNT_MUTE_SUCCESS:
|
||||
return filterContexts(state, action.relationship, action.statuses);
|
||||
case CONTEXT_FETCH_SUCCESS:
|
||||
return normalizeContext(state, action.id, action.ancestors, action.descendants);
|
||||
return normalizeContext(state, action.id, action.ancestors, action.descendants, action.references);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteFromContexts(state, [action.id]);
|
||||
case TIMELINE_UPDATE:
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
REBLOGS_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
EMOJI_REACTIONS_FETCH_SUCCESS,
|
||||
STATUS_REFERENCES_FETCH_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
MUTES_FETCH_REQUEST,
|
||||
|
@ -75,6 +76,7 @@ const initialState = ImmutableMap({
|
|||
reblogged_by: initialListState,
|
||||
favourited_by: initialListState,
|
||||
emoji_reactioned_by: initialListState,
|
||||
referred_by: initialListState,
|
||||
follow_requests: initialListState,
|
||||
blocks: 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)));
|
||||
case EMOJI_REACTIONS_FETCH_SUCCESS:
|
||||
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:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
|
|
|
@ -6,6 +6,9 @@ $classic-primary-color: #9baec8;
|
|||
$classic-secondary-color: #d9e1e8;
|
||||
$classic-highlight-color: #6364ff;
|
||||
|
||||
$emoji-reaction-color: #42485a !default;
|
||||
$emoji-reaction-selected-color: #617ed5 !default;
|
||||
|
||||
$ui-base-color: $classic-base-color !default;
|
||||
$ui-primary-color: $classic-primary-color !default;
|
||||
$ui-secondary-color: $classic-secondary-color !default;
|
||||
|
|
|
@ -13,6 +13,9 @@ $blurple-300: #858afa; // Faded Blue
|
|||
$grey-600: #4e4c5a; // Trout
|
||||
$grey-100: #dadaf3; // Topaz
|
||||
|
||||
$emoji-reaction-color: #dfe5f5 !default;
|
||||
$emoji-reaction-selected-color: #9ac1f2 !default;
|
||||
|
||||
// Differences
|
||||
$success-green: lighten(#3c754d, 8%);
|
||||
|
||||
|
|
|
@ -1326,7 +1326,7 @@ body > [data-popper-placement] {
|
|||
margin: 12px 0 2px 4px;
|
||||
|
||||
.emoji-reactions-bar__button {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
background: $emoji-reaction-color;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
@ -1335,7 +1335,7 @@ body > [data-popper-placement] {
|
|||
height: 24px;
|
||||
|
||||
&.toggled {
|
||||
background: darken($ui-primary-color, 16%);
|
||||
background: $emoji-reaction-selected-color;
|
||||
}
|
||||
|
||||
> .emoji {
|
||||
|
@ -2128,7 +2128,7 @@ a.account__display-name {
|
|||
font: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
|
|
|
@ -23,6 +23,10 @@ $classic-primary-color: #9baec8; // Echo Blue
|
|||
$classic-secondary-color: #d9e1e8; // Pattens Blue
|
||||
$classic-highlight-color: #6364ff; // Brand purple
|
||||
|
||||
// Values for kmyblue original functions
|
||||
$emoji-reaction-color: #42485a !default;
|
||||
$emoji-reaction-selected-color: #617ed5 !default;
|
||||
|
||||
// Variables for defaults in UI
|
||||
$base-shadow-color: $black !default;
|
||||
$base-overlay-background: $black !default;
|
||||
|
|
|
@ -97,6 +97,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
fetch_replies(@status)
|
||||
distribute
|
||||
forward_for_reply
|
||||
process_references!
|
||||
join_group!
|
||||
end
|
||||
|
||||
|
@ -471,6 +472,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
retry
|
||||
end
|
||||
|
||||
def process_references!
|
||||
references = @json['references'].nil? ? [] : ActivityPub::FetchReferencesService(@json['references'])
|
||||
ProcessReferencesWorker.perform_async(@status.id, [], references)
|
||||
end
|
||||
|
||||
def join_group!
|
||||
GroupReblogService.new.call(@status)
|
||||
end
|
||||
|
|
|
@ -7,11 +7,48 @@ require 'resolv'
|
|||
# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
|
||||
# around the Socket#open method, since we use our own timeout blocks inside
|
||||
# that method
|
||||
#
|
||||
# Also changes how the read timeout behaves so that it is cumulative (closer
|
||||
# to HTTP::Timeout::Global, but still having distinct timeouts for other
|
||||
# operation types)
|
||||
class HTTP::Timeout::PerOperation
|
||||
def connect(socket_class, host, port, nodelay = false)
|
||||
@socket = socket_class.open(host, port)
|
||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
||||
end
|
||||
|
||||
# Reset deadline when the connection is re-used for different requests
|
||||
def reset_counter
|
||||
@deadline = nil
|
||||
end
|
||||
|
||||
# Read data from the socket
|
||||
def readpartial(size, buffer = nil)
|
||||
@deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_timeout
|
||||
|
||||
timeout = false
|
||||
loop do
|
||||
result = @socket.read_nonblock(size, buffer, exception: false)
|
||||
|
||||
return :eof if result.nil?
|
||||
|
||||
remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout || remaining_time <= 0
|
||||
return result if result != :wait_readable
|
||||
|
||||
# marking the socket for timeout. Why is this not being raised immediately?
|
||||
# it seems there is some race-condition on the network level between calling
|
||||
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
||||
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
||||
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
||||
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
||||
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
||||
# timeout. Else, the first timeout was a proper timeout.
|
||||
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
||||
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
||||
timeout = true unless @socket.to_io.wait_readable(remaining_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
|
|
|
@ -53,6 +53,26 @@ class TextFormatter
|
|||
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
class << self
|
||||
include ERB::Util
|
||||
|
||||
def shortened_link(url, rel_me: false)
|
||||
url = Addressable::URI.parse(url).to_s
|
||||
rel = rel_me ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
|
||||
|
||||
prefix = url.match(URL_PREFIX_REGEX).to_s
|
||||
display_url = url[prefix.length, 30]
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
||||
<<~HTML.squish
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}" translate="no"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
HTML
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
h(url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rewrite
|
||||
|
@ -75,19 +95,7 @@ class TextFormatter
|
|||
end
|
||||
|
||||
def link_to_url(entity)
|
||||
url = Addressable::URI.parse(entity[:url]).to_s
|
||||
rel = with_rel_me? ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
|
||||
|
||||
prefix = url.match(URL_PREFIX_REGEX).to_s
|
||||
display_url = url[prefix.length, 30]
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
||||
<<~HTML.squish
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}" translate="no"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
HTML
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
h(entity[:url])
|
||||
TextFormatter.shortened_link(entity[:url], rel_me: with_rel_me?)
|
||||
end
|
||||
|
||||
def link_to_hashtag(entity)
|
||||
|
|
|
@ -20,6 +20,7 @@ class AccountWarning < ApplicationRecord
|
|||
enum action: {
|
||||
none: 0,
|
||||
disable: 1_000,
|
||||
force_cw: 1_200,
|
||||
mark_statuses_as_sensitive: 1_250,
|
||||
delete_statuses: 1_500,
|
||||
sensitive: 2_000,
|
||||
|
|
|
@ -33,6 +33,8 @@ class Admin::StatusBatchAction
|
|||
handle_delete!
|
||||
when 'mark_as_sensitive'
|
||||
handle_mark_as_sensitive!
|
||||
when 'force_cw'
|
||||
handle_force_cw!
|
||||
when 'report'
|
||||
handle_report!
|
||||
when 'remove_from_report'
|
||||
|
@ -104,6 +106,42 @@ class Admin::StatusBatchAction
|
|||
UserMailer.warning(target_account.user, @warning).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def handle_force_cw!
|
||||
representative_account = Account.representative
|
||||
|
||||
# Can't use a transaction here because UpdateStatusService queues
|
||||
# Sidekiq jobs
|
||||
statuses.find_each do |status|
|
||||
authorize([:admin, status], :update?)
|
||||
|
||||
status_text = status.text
|
||||
status_text = "#{status.spoiler_text}\n\n#{status_text}" if status.spoiler_text
|
||||
|
||||
if target_account.local?
|
||||
UpdateStatusService.new.call(status, representative_account.id, spoiler_text: 'CW', text: status_text)
|
||||
else
|
||||
status.update(spoiler_text: 'CW', text: status_text)
|
||||
end
|
||||
|
||||
log_action(:update, status)
|
||||
|
||||
if with_report?
|
||||
report.resolve!(current_account)
|
||||
log_action(:resolve, report)
|
||||
end
|
||||
end
|
||||
|
||||
@warning = target_account.strikes.create!(
|
||||
action: :force_cw,
|
||||
account: current_account,
|
||||
report: report,
|
||||
status_ids: status_ids,
|
||||
text: text
|
||||
)
|
||||
|
||||
UserMailer.warning(target_account.user, @warning).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def handle_report!
|
||||
@report = Report.new(report_params) unless with_report?
|
||||
@report.status_ids = (@report.status_ids + allowed_status_ids).uniq
|
||||
|
|
|
@ -22,15 +22,14 @@ module Attachmentable
|
|||
|
||||
included do
|
||||
def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
|
||||
options = { validate_media_type: false }.merge(options)
|
||||
super(name, options)
|
||||
send(:"before_#{name}_post_process") do
|
||||
|
||||
send(:"before_#{name}_validate") do
|
||||
attachment = send(name)
|
||||
check_image_dimension(attachment)
|
||||
set_file_content_type(attachment)
|
||||
obfuscate_file_name(attachment)
|
||||
set_file_extension(attachment)
|
||||
Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(attributes: [name]).validate(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Context < ActiveModelSerializers::Model
|
||||
attributes :ancestors, :descendants
|
||||
attributes :ancestors, :descendants, :references
|
||||
end
|
||||
|
|
|
@ -26,6 +26,7 @@ class Notification < ApplicationRecord
|
|||
'FollowRequest' => :follow_request,
|
||||
'Favourite' => :favourite,
|
||||
'EmojiReaction' => :emoji_reaction,
|
||||
'StatusReference' => :status_reference,
|
||||
'Poll' => :poll,
|
||||
}.freeze
|
||||
|
||||
|
@ -33,6 +34,7 @@ class Notification < ApplicationRecord
|
|||
mention
|
||||
status
|
||||
reblog
|
||||
status_reference
|
||||
follow
|
||||
follow_request
|
||||
favourite
|
||||
|
@ -47,6 +49,7 @@ class Notification < ApplicationRecord
|
|||
TARGET_STATUS_INCLUDES_BY_TYPE = {
|
||||
status: :status,
|
||||
reblog: [status: :reblog],
|
||||
status_reference: [status_reference: :status],
|
||||
mention: [mention: :status],
|
||||
favourite: [favourite: :status],
|
||||
emoji_reaction: [emoji_reaction: :status],
|
||||
|
@ -67,6 +70,7 @@ class Notification < ApplicationRecord
|
|||
belongs_to :follow_request, inverse_of: :notification
|
||||
belongs_to :favourite, inverse_of: :notification
|
||||
belongs_to :emoji_reaction, inverse_of: :notification
|
||||
belongs_to :status_reference, inverse_of: :notification
|
||||
belongs_to :poll, inverse_of: false
|
||||
belongs_to :report, inverse_of: false
|
||||
end
|
||||
|
@ -85,6 +89,8 @@ class Notification < ApplicationRecord
|
|||
status
|
||||
when :reblog
|
||||
status&.reblog
|
||||
when :status_reference
|
||||
status_reference&.status
|
||||
when :favourite
|
||||
favourite&.status
|
||||
when :emoji_reaction, :reaction
|
||||
|
@ -136,6 +142,8 @@ class Notification < ApplicationRecord
|
|||
notification.status = cached_status
|
||||
when :reblog
|
||||
notification.status.reblog = cached_status
|
||||
when :status_reference
|
||||
notification.status_reference.status = cached_status
|
||||
when :favourite
|
||||
notification.favourite.status = cached_status
|
||||
when :emoji_reaction, :reaction
|
||||
|
@ -162,7 +170,7 @@ class Notification < ApplicationRecord
|
|||
case activity_type
|
||||
when 'Status', 'Follow', 'Favourite', 'EmojiReaction', 'EmojiReact', 'FollowRequest', 'Poll', 'Report'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'Mention'
|
||||
when 'Mention', 'StatusReference'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
when 'Account'
|
||||
self.from_account_id = activity&.id
|
||||
|
|
|
@ -75,6 +75,10 @@ class Status < ApplicationRecord
|
|||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :reference_objects, class_name: 'StatusReference', inverse_of: :status, dependent: :destroy
|
||||
has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status
|
||||
has_many :referenced_by_status_objects, foreign_key: 'target_status_id', class_name: 'StatusReference', inverse_of: :target_status, dependent: :destroy
|
||||
has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status
|
||||
|
||||
has_and_belongs_to_many :tags
|
||||
has_and_belongs_to_many :preview_cards
|
||||
|
@ -332,6 +336,10 @@ class Status < ApplicationRecord
|
|||
status_stat&.emoji_reaction_accounts_count || 0
|
||||
end
|
||||
|
||||
def status_referred_by_count
|
||||
status_stat&.status_referred_by_count || 0
|
||||
end
|
||||
|
||||
def increment_count!(key)
|
||||
update_status_stat!(key => public_send(key) + 1)
|
||||
end
|
||||
|
@ -340,6 +348,10 @@ class Status < ApplicationRecord
|
|||
update_status_stat!(key => [public_send(key) - 1, 0].max)
|
||||
end
|
||||
|
||||
def add_status_referred_by_count!(diff)
|
||||
update_status_stat!(status_referred_by_count: [public_send(:status_referred_by_count) + diff, 0].max)
|
||||
end
|
||||
|
||||
def emoji_reactions_grouped_by_name(account = nil)
|
||||
(Oj.load(status_stat&.emoji_reactions || '', mode: :strict) || []).tap do |emoji_reactions|
|
||||
if account.present?
|
||||
|
|
25
app/models/status_reference.rb
Normal file
25
app/models/status_reference.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: status_references
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_id :bigint(8) not null
|
||||
# target_status_id :bigint(8) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class StatusReference < ApplicationRecord
|
||||
belongs_to :status
|
||||
belongs_to :target_status, class_name: 'Status'
|
||||
|
||||
has_one :notification, as: :activity, dependent: :destroy
|
||||
|
||||
validate :validate_status_visibilities
|
||||
|
||||
def validate_status_visibilities
|
||||
raise Mastodon::ValidationError, I18n.t('status_references.errors.invalid_status_visibilities') if [:public, :public_unlisted, :unlisted, :login].exclude?(target_status.visibility.to_sym)
|
||||
end
|
||||
end
|
|
@ -15,6 +15,7 @@
|
|||
# emoji_reactions_count :integer default(0), not null
|
||||
# test :integer default(0), not null
|
||||
# emoji_reaction_accounts_count :integer default(0), not null
|
||||
# status_referred_by_count :integer default(0), not null
|
||||
#
|
||||
|
||||
class StatusStat < ApplicationRecord
|
||||
|
@ -46,6 +47,10 @@ class StatusStat < ApplicationRecord
|
|||
[attributes['emoji_reaction_accounts_count'], 0].max
|
||||
end
|
||||
|
||||
def status_referred_by_count
|
||||
[attributes['status_referred_by_count'] || 0, 0].max
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_parent_cache
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
class REST::ContextSerializer < ActiveModel::Serializer
|
||||
has_many :ancestors, serializer: REST::StatusSerializer
|
||||
has_many :descendants, serializer: REST::StatusSerializer
|
||||
has_many :references, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
|
|
@ -117,6 +117,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
|||
:kmyblue_markdown,
|
||||
:kmyblue_reaction_deck,
|
||||
:kmyblue_visibility_login,
|
||||
:status_reference,
|
||||
]
|
||||
|
||||
capabilities << :profile_search unless Chewy.enabled?
|
||||
|
|
|
@ -13,7 +13,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def status_type?
|
||||
[:favourite, :emoji_reaction, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
||||
[:favourite, :emoji_reaction, :reaction, :reblog, :status_reference, :status, :mention, :poll, :update].include?(object.type)
|
||||
end
|
||||
|
||||
def report_type?
|
||||
|
|
|
@ -11,4 +11,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
|||
def image
|
||||
object.image? ? full_asset_url(object.image.url(:original)) : nil
|
||||
end
|
||||
|
||||
def html
|
||||
Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
|
||||
:sensitive, :spoiler_text, :visibility, :visibility_ex, :language,
|
||||
:uri, :url, :replies_count, :reblogs_count, :searchability, :markdown,
|
||||
:status_reference_ids, :status_references_count, :status_referred_by_count,
|
||||
:favourites_count, :emoji_reactions, :emoji_reactions_count, :reactions, :edited_at
|
||||
|
||||
attribute :favourited, if: :current_user?
|
||||
|
@ -92,6 +93,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
ActivityPub::TagManager.instance.url_for(object)
|
||||
end
|
||||
|
||||
def status_reference_ids
|
||||
@status_reference_ids = object.reference_objects.pluck(:target_status_id)
|
||||
end
|
||||
|
||||
def status_references_count
|
||||
status_reference_ids.size
|
||||
end
|
||||
|
||||
def favourited
|
||||
if instance_options && instance_options[:relationships]
|
||||
instance_options[:relationships].favourites_map[object.id] || false
|
||||
|
|
|
@ -126,6 +126,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
|
|||
:kmyblue_markdown,
|
||||
:kmyblue_reaction_deck,
|
||||
:kmyblue_visibility_login,
|
||||
:status_reference,
|
||||
]
|
||||
|
||||
capabilities << :profile_search unless Chewy.enabled?
|
||||
|
|
49
app/services/activitypub/fetch_references_service.rb
Normal file
49
app/services/activitypub/fetch_references_service.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::FetchReferencesService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
def call(status, collection_or_uri)
|
||||
@account = status.account
|
||||
|
||||
collection_items(collection_or_uri)&.map { |item| value_or_id(item) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_items(collection_or_uri)
|
||||
collection = fetch_collection(collection_or_uri)
|
||||
return unless collection.is_a?(Hash) && collection['first'].present?
|
||||
|
||||
all_items = []
|
||||
collection = fetch_collection(collection['first'])
|
||||
|
||||
while collection.is_a?(Hash)
|
||||
items = begin
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
end
|
||||
end
|
||||
|
||||
break if items.blank?
|
||||
|
||||
all_items.concat(items)
|
||||
|
||||
break if all_items.size >= StatusReferenceValidator::LIMIT
|
||||
|
||||
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
||||
end
|
||||
|
||||
all_items
|
||||
end
|
||||
|
||||
def fetch_collection(collection_or_uri)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return if invalid_origin?(collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
end
|
||||
end
|
|
@ -29,6 +29,8 @@ class ApproveAppealService < BaseService
|
|||
undo_delete_statuses!
|
||||
when 'mark_statuses_as_sensitive'
|
||||
undo_mark_statuses_as_sensitive!
|
||||
when 'force_cw'
|
||||
undo_force_cw!
|
||||
when 'sensitive'
|
||||
undo_sensitive!
|
||||
when 'silence'
|
||||
|
@ -58,6 +60,13 @@ class ApproveAppealService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def undo_force_cw!
|
||||
representative_account = Account.representative
|
||||
@strike.statuses.includes(:media_attachments).each do |status|
|
||||
UpdateStatusService.new.call(status, representative_account.id, spoiler_text: '')
|
||||
end
|
||||
end
|
||||
|
||||
def undo_sensitive!
|
||||
target_account.unsensitize!
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ class PostStatusService < BaseService
|
|||
# @option [String] :idempotency Optional idempotency key
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
|
||||
# @option [Enumerable] :status_reference_ids Optional array
|
||||
# @return [Status]
|
||||
def call(account, options = {})
|
||||
@account = account
|
||||
|
@ -78,6 +79,7 @@ class PostStatusService < BaseService
|
|||
@markdown = @options[:markdown] || false
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
@reference_ids = (@options[:status_reference_ids] || []).map(&:to_i).filter(&:positive?)
|
||||
rescue ArgumentError
|
||||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
@ -146,6 +148,7 @@ class PostStatusService < BaseService
|
|||
|
||||
def postprocess_status!
|
||||
process_hashtags_service.call(@status)
|
||||
ProcessReferencesWorker.perform_async(@status.id, @reference_ids, [])
|
||||
Trends.tags.register(@status)
|
||||
LinkCrawlWorker.perform_async(@status.id)
|
||||
DistributionWorker.perform_async(@status.id)
|
||||
|
@ -221,6 +224,7 @@ class PostStatusService < BaseService
|
|||
media_attachments: @media || [],
|
||||
ordered_media_attachment_ids: (@options[:media_ids] || []).map(&:to_i) & @media.map(&:id),
|
||||
thread: @in_reply_to,
|
||||
status_reference_ids: @status_reference_ids,
|
||||
poll_attributes: poll_attributes,
|
||||
sensitive: @sensitive,
|
||||
spoiler_text: @options[:spoiler_text] || '',
|
||||
|
|
88
app/services/process_references_service.rb
Normal file
88
app/services/process_references_service.rb
Normal file
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessReferencesService < BaseService
|
||||
include Payloadable
|
||||
|
||||
DOMAIN = ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
|
||||
REFURL_EXP = /(RT|QT|BT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
|
||||
|
||||
def call(status, reference_parameters, save_records: true, urls: nil)
|
||||
@status = status
|
||||
@reference_parameters = reference_parameters || []
|
||||
@save_records = save_records
|
||||
@urls = urls || []
|
||||
|
||||
old_references
|
||||
|
||||
return unless added_references.size.positive? || removed_references.size.positive?
|
||||
|
||||
StatusReference.transaction do
|
||||
remove_old_references
|
||||
add_references
|
||||
|
||||
@status.save! if @save_records
|
||||
|
||||
create_notifications!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def references
|
||||
@references = @reference_parameters + scan_text!
|
||||
end
|
||||
|
||||
def old_references
|
||||
@old_references = @status.references.pluck(:id)
|
||||
end
|
||||
|
||||
def added_references
|
||||
(references - old_references).uniq
|
||||
end
|
||||
|
||||
def removed_references
|
||||
(old_references - references).uniq
|
||||
end
|
||||
|
||||
def scan_text!
|
||||
text = @status.account.local? ? @status.text : @status.text.gsub(%r{</?[^>]*>}, '')
|
||||
@scan_text = fetch_statuses!(text.scan(REFURL_EXP).pluck(3).uniq).map(&:id).uniq.filter { |status_id| !status_id.zero? }
|
||||
end
|
||||
|
||||
def fetch_statuses!(urls)
|
||||
(urls + @urls)
|
||||
.map { |url| ResolveURLService.new.call(url) }
|
||||
.filter { |status| status }
|
||||
end
|
||||
|
||||
def add_references
|
||||
return if added_references.empty?
|
||||
|
||||
@added_objects = []
|
||||
|
||||
statuses = Status.where(id: added_references)
|
||||
statuses.each do |status|
|
||||
@added_objects << @status.reference_objects.new(target_status: status)
|
||||
status.increment_count!(:status_referred_by_count)
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications!
|
||||
local_reference_objects = @added_objects.filter { |ref| ref.target_status.account.local? }
|
||||
return if local_reference_objects.empty?
|
||||
|
||||
LocalNotificationWorker.push_bulk(local_reference_objects) do |ref|
|
||||
[ref.target_status.account_id, ref.id, 'StatusReference', 'status_reference']
|
||||
end
|
||||
end
|
||||
|
||||
def remove_old_references
|
||||
return if removed_references.empty?
|
||||
|
||||
statuses = Status.where(id: removed_references)
|
||||
@status.reference_objects.where(target_status: statuses).destroy_all
|
||||
statuses.each do |status|
|
||||
status.decrement_count!(:status_referred_by_count)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,6 +46,7 @@ class RemoveStatusService < BaseService
|
|||
remove_from_public
|
||||
remove_from_media if @status.with_media?
|
||||
remove_media
|
||||
decrement_references
|
||||
end
|
||||
|
||||
@status.destroy! if permanently?
|
||||
|
@ -123,6 +124,12 @@ class RemoveStatusService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def decrement_references
|
||||
@status.references.each do |ref|
|
||||
ref.decrement_count!(:status_referred_by_count)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_hashtags
|
||||
@account.featured_tags.where(tag_id: @status.tags.map(&:id)).each do |featured_tag|
|
||||
featured_tag.decrement(@status.id)
|
||||
|
|
|
@ -142,6 +142,7 @@ class UpdateStatusService < BaseService
|
|||
def update_metadata!
|
||||
ProcessHashtagsService.new.call(@status)
|
||||
ProcessMentionsService.new.call(@status)
|
||||
ProcessReferencesWorker.perform_async(@status.id, (@options[:status_reference_ids] || []).map(&:to_i).filter(&:positive?), [])
|
||||
end
|
||||
|
||||
def broadcast_updates!
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
= button_tag t('admin.reports.mark_as_sensitive'), name: :mark_as_sensitive, class: 'button'
|
||||
.report-actions__item__description
|
||||
= t('admin.reports.actions.mark_as_sensitive_description_html')
|
||||
.report-actions__item
|
||||
.report-actions__item__button
|
||||
= button_tag t('admin.reports.force_cw'), name: :force_cw, class: 'button'
|
||||
.report-actions__item__description
|
||||
= t('admin.reports.actions.force_cw_description_html')
|
||||
.report-actions__item
|
||||
.report-actions__item__button
|
||||
= button_tag t('admin.reports.delete_and_resolve'), name: :delete, class: 'button button--destructive'
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
= t('antennas.index.tags', count: antenna.antenna_tags.size)
|
||||
.permissions-list__item__text__type
|
||||
- tags = antenna.antenna_tags.map { |tag| tag.tag.name }
|
||||
- tags = keywords.take(5) + ['…'] if tags.size > 5 # TODO
|
||||
- tags = tags.take(5) + ['…'] if tags.size > 5 # TODO
|
||||
= tags.join(', ')
|
||||
|
||||
.announcements-list__item__action-bar
|
||||
|
|
11
app/workers/process_references_worker.rb
Normal file
11
app/workers/process_references_worker.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessReferencesWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(status_id, ids, urls)
|
||||
ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls || [])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue