Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/api/v1/statuses_controller.rb`: Upstream moved things around in a place where glitch-soc had support for an extra parameter (`content_type`). Follow upstream but reintroduce `content_type`.
This commit is contained in:
@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
|
||||
end
|
||||
|
||||
def update
|
||||
@media_attachment.update!(media_attachment_params)
|
||||
@media_attachment.update!(updateable_media_attachment_params)
|
||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||
end
|
||||
|
||||
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
|
||||
params.permit(:file, :thumbnail, :description, :focus)
|
||||
end
|
||||
|
||||
def updateable_media_attachment_params
|
||||
params.permit(:thumbnail, :description, :focus)
|
||||
end
|
||||
|
||||
def file_type_error
|
||||
{ error: 'File type of uploaded media could not be verified' }
|
||||
end
|
||||
|
@ -33,6 +33,6 @@ class Api::V1::ReportsController < Api::BaseController
|
||||
end
|
||||
|
||||
def report_params
|
||||
params.permit(:account_id, :comment, :forward, status_ids: [])
|
||||
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
|
||||
end
|
||||
end
|
||||
|
@ -3,8 +3,8 @@
|
||||
class Api::V1::StatusesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||
before_action :require_user!, except: [:show, :context]
|
||||
before_action :set_status, only: [:show, :context]
|
||||
before_action :set_thread, only: [:create]
|
||||
@ -35,25 +35,45 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
@status = PostStatusService.new.call(current_user.account,
|
||||
text: status_params[:status],
|
||||
thread: @thread,
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
with_rate_limit: true)
|
||||
@status = PostStatusService.new.call(
|
||||
current_user.account,
|
||||
text: status_params[:status],
|
||||
thread: @thread,
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
language: status_params[:language],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
with_rate_limit: true
|
||||
)
|
||||
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@status = Status.where(account: current_account).find(params[:id])
|
||||
authorize @status, :update?
|
||||
|
||||
UpdateStatusService.new.call(
|
||||
@status,
|
||||
current_account.id,
|
||||
text: status_params[:status],
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
poll: status_params[:poll]
|
||||
)
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
||||
@status = Status.where(account: current_account).find(params[:id])
|
||||
authorize @status, :destroy?
|
||||
|
||||
@status.discard
|
||||
@ -85,6 +105,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
:language,
|
||||
:scheduled_at,
|
||||
:content_type,
|
||||
media_ids: [],
|
||||
|
@ -70,6 +70,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||
|
||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
@ -83,6 +85,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
}
|
||||
};
|
||||
|
||||
export function setComposeToStatus(status, text, spoiler_text) {
|
||||
return{
|
||||
type: COMPOSE_SET_STATUS,
|
||||
status,
|
||||
text,
|
||||
spoiler_text,
|
||||
};
|
||||
};
|
||||
|
||||
export function changeCompose(text) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE,
|
||||
@ -137,8 +148,9 @@ export function directCompose(account, routerHistory) {
|
||||
|
||||
export function submitCompose(routerHistory) {
|
||||
return function (dispatch, getState) {
|
||||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const statusId = getState().getIn(['compose', 'id'], null);
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
return;
|
||||
@ -146,15 +158,18 @@ export function submitCompose(routerHistory) {
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
api(getState).post('/api/v1/statuses', {
|
||||
status,
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
}, {
|
||||
api(getState).request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
data: {
|
||||
status,
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
},
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
@ -176,11 +191,11 @@ export function submitCompose(routerHistory) {
|
||||
}
|
||||
};
|
||||
|
||||
if (response.data.visibility !== 'direct') {
|
||||
if (statusId === null && response.data.visibility !== 'direct') {
|
||||
insertIfOnline('home');
|
||||
}
|
||||
|
||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||
insertIfOnline('community');
|
||||
if (!response.data.local_only) {
|
||||
insertIfOnline('public');
|
||||
|
@ -2,7 +2,7 @@ import api from '../api';
|
||||
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
|
||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||
@ -30,6 +30,10 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
||||
|
||||
export const REDRAFT = 'REDRAFT';
|
||||
|
||||
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
||||
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
||||
|
||||
export function fetchStatusRequest(id, skipLoading) {
|
||||
return {
|
||||
type: STATUS_FETCH_REQUEST,
|
||||
@ -84,6 +88,37 @@ export function redraft(status, raw_text) {
|
||||
};
|
||||
};
|
||||
|
||||
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
||||
if (status.get('poll')) {
|
||||
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||
}
|
||||
|
||||
dispatch(fetchStatusSourceRequest());
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch(fetchStatusSourceSuccess());
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusSourceFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchStatusSourceRequest = () => ({
|
||||
type: STATUS_FETCH_SOURCE_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchStatusSourceSuccess = () => ({
|
||||
type: STATUS_FETCH_SOURCE_SUCCESS,
|
||||
});
|
||||
|
||||
export const fetchStatusSourceFail = error => ({
|
||||
type: STATUS_FETCH_SOURCE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
@ -12,6 +12,7 @@ import classNames from 'classnames';
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
@ -137,6 +138,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handlePinClick = () => {
|
||||
this.props.onPin(this.props.status);
|
||||
}
|
||||
@ -255,6 +260,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (writtenByMe) {
|
||||
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusCollapse,
|
||||
editStatus,
|
||||
} from '../actions/statuses';
|
||||
import {
|
||||
unmuteAccount,
|
||||
@ -142,6 +143,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onEdit (status, history) {
|
||||
dispatch(editStatus(status.get('id'), history));
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
@ -29,6 +29,7 @@ const messages = defineMessages({
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@ -50,6 +51,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
@ -200,7 +202,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
const disabled = this.props.isSubmitting;
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
if (this.props.isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||
} else {
|
||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
|
@ -6,9 +6,20 @@ import ReplyIndicator from '../components/reply_indicator';
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
let statusId = state.getIn(['compose', 'id'], null);
|
||||
let editing = true;
|
||||
|
||||
if (statusId === null) {
|
||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }),
|
||||
editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import classNames from 'classnames';
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
@ -59,6 +60,7 @@ class ActionBar extends React.PureComponent {
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
@ -98,6 +100,10 @@ class ActionBar extends React.PureComponent {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
@ -209,6 +215,7 @@ class ActionBar extends React.PureComponent {
|
||||
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
muteStatus,
|
||||
unmuteStatus,
|
||||
deleteStatus,
|
||||
editStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
} from '../../actions/statuses';
|
||||
@ -273,6 +274,10 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleEditClick = (status, history) => {
|
||||
this.props.dispatch(editStatus(status.get('id'), history));
|
||||
}
|
||||
|
||||
handleDirectClick = (account, router) => {
|
||||
this.props.dispatch(directCompose(account, router));
|
||||
}
|
||||
@ -567,6 +572,7 @@ class Status extends ImmutablePureComponent {
|
||||
onReblog={this.handleReblogClick}
|
||||
onBookmark={this.handleBookmarkClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onEdit={this.handleEditClick}
|
||||
onDirect={this.handleDirectClick}
|
||||
onMention={this.handleMentionClick}
|
||||
onMute={this.handleMuteClick}
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
INIT_MEDIA_EDIT_MODAL,
|
||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||
COMPOSE_SET_STATUS,
|
||||
} from '../actions/compose';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
@ -58,6 +59,7 @@ const initialState = ImmutableMap({
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
privacy: null,
|
||||
id: null,
|
||||
text: '',
|
||||
focusDate: null,
|
||||
caretPosition: null,
|
||||
@ -107,6 +109,7 @@ function statusToTextMentions(state, status) {
|
||||
|
||||
function clearAll(state) {
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('text', '');
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
@ -313,6 +316,7 @@ export default function compose(state = initialState, action) {
|
||||
return state.set('is_composing', action.value);
|
||||
case COMPOSE_REPLY:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', null);
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
@ -329,21 +333,12 @@ export default function compose(state = initialState, action) {
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
});
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
case COMPOSE_RESET:
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', null);
|
||||
map.set('text', '');
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('poll', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||
return state.set('is_changing_upload', true);
|
||||
case COMPOSE_REPLY_CANCEL:
|
||||
case COMPOSE_RESET:
|
||||
case COMPOSE_SUBMIT_SUCCESS:
|
||||
return clearAll(state);
|
||||
case COMPOSE_SUBMIT_FAIL:
|
||||
@ -454,6 +449,34 @@ export default function compose(state = initialState, action) {
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
map.set('poll', ImmutableMap({
|
||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||
multiple: action.status.getIn(['poll', 'multiple']),
|
||||
expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
|
||||
}));
|
||||
}
|
||||
});
|
||||
case COMPOSE_SET_STATUS:
|
||||
return state.withMutations(map => {
|
||||
map.set('id', action.status.get('id'));
|
||||
map.set('text', action.text);
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
map.set('media_attachments', action.status.get('media_attachments'));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('sensitive', action.status.get('sensitive'));
|
||||
|
||||
if (action.spoiler_text.length > 0) {
|
||||
map.set('spoiler', true);
|
||||
map.set('spoiler_text', action.spoiler_text);
|
||||
} else {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
map.set('poll', ImmutableMap({
|
||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||
|
@ -208,6 +208,10 @@ class MediaAttachment < ApplicationRecord
|
||||
file.blank? && remote_url.present?
|
||||
end
|
||||
|
||||
def significantly_changed?
|
||||
description_previously_changed? || thumbnail_updated_at_previously_changed? || file_meta_previously_changed?
|
||||
end
|
||||
|
||||
def larger_media_format?
|
||||
video? || gifv? || audio?
|
||||
end
|
||||
|
@ -83,6 +83,12 @@ class Poll < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def reset_votes!
|
||||
self.cached_tallies = options.map { 0 }
|
||||
self.votes_count = 0
|
||||
votes.delete_all unless new_record?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_cached_tallies
|
||||
|
@ -39,6 +39,9 @@ class Report < ApplicationRecord
|
||||
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
|
||||
|
||||
validates :comment, length: { maximum: 1_000 }
|
||||
validates :rule_ids, absence: true, unless: :violation?
|
||||
|
||||
validate :validate_rule_ids
|
||||
|
||||
enum category: {
|
||||
other: 0,
|
||||
@ -122,4 +125,10 @@ class Report < ApplicationRecord
|
||||
def set_uri
|
||||
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
||||
end
|
||||
|
||||
def validate_rule_ids
|
||||
return unless violation?
|
||||
|
||||
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids.size
|
||||
end
|
||||
end
|
||||
|
@ -215,6 +215,16 @@ class Status < ApplicationRecord
|
||||
public_visibility? || unlisted_visibility?
|
||||
end
|
||||
|
||||
def snapshot!(media_attachments_changed: false, account_id: nil, at_time: nil)
|
||||
edits.create!(
|
||||
text: text,
|
||||
spoiler_text: spoiler_text,
|
||||
media_attachments_changed: media_attachments_changed,
|
||||
account_id: account_id || self.account_id,
|
||||
created_at: at_time || edited_at
|
||||
)
|
||||
end
|
||||
|
||||
def edited?
|
||||
edited_at.present?
|
||||
end
|
||||
|
@ -39,7 +39,7 @@ class StatusPolicy < ApplicationPolicy
|
||||
alias unreblog? destroy?
|
||||
|
||||
def update?
|
||||
staff?
|
||||
staff? || owned?
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -95,10 +95,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
|
||||
# If for some reasons the options were changed, it invalidates all previous
|
||||
# votes, so we need to remove them
|
||||
if poll_parser.significantly_changes?(poll)
|
||||
@poll_changed = true
|
||||
poll.votes.delete_all unless poll.new_record?
|
||||
end
|
||||
@poll_changed = true if poll_parser.significantly_changes?(poll)
|
||||
|
||||
poll.last_fetched_at = Time.now.utc
|
||||
poll.options = poll_parser.options
|
||||
@ -106,6 +103,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
poll.expires_at = poll_parser.expires_at
|
||||
poll.voters_count = poll_parser.voters_count
|
||||
poll.cached_tallies = poll_parser.cached_tallies
|
||||
poll.reset_votes! if @poll_changed
|
||||
poll.save!
|
||||
|
||||
@status.poll_id = poll.id
|
||||
@ -217,24 +215,18 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
|
||||
return if @status.edits.any?
|
||||
|
||||
@status.edits.create(
|
||||
text: @status.text,
|
||||
spoiler_text: @status.spoiler_text,
|
||||
@status.snapshot!(
|
||||
media_attachments_changed: false,
|
||||
account_id: @account.id,
|
||||
created_at: @status.created_at
|
||||
at_time: @status.created_at
|
||||
)
|
||||
end
|
||||
|
||||
def create_edit!
|
||||
return unless significant_changes?
|
||||
|
||||
@status_edit = @status.edits.create(
|
||||
text: @status.text,
|
||||
spoiler_text: @status.spoiler_text,
|
||||
@status.snapshot!(
|
||||
media_attachments_changed: @media_attachments_changed || @poll_changed,
|
||||
account_id: @account.id,
|
||||
created_at: @status.edited_at
|
||||
account_id: @account.id
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -1,20 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProcessHashtagsService < BaseService
|
||||
def call(status, tags = [])
|
||||
tags = Extractor.extract_hashtags(status.text) if status.local?
|
||||
records = []
|
||||
def call(status, raw_tags = [])
|
||||
@status = status
|
||||
@account = status.account
|
||||
@raw_tags = status.local? ? Extractor.extract_hashtags(status.text) : raw_tags
|
||||
@previous_tags = status.tags.to_a
|
||||
@current_tags = []
|
||||
|
||||
Tag.find_or_create_by_names(tags) do |tag|
|
||||
status.tags << tag
|
||||
records << tag
|
||||
tag.update(last_status_at: status.created_at) if tag.last_status_at.nil? || (tag.last_status_at < status.created_at && tag.last_status_at < 12.hours.ago)
|
||||
assign_tags!
|
||||
update_featured_tags!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_tags!
|
||||
@status.tags = @current_tags = Tag.find_or_create_by_names(@raw_tags)
|
||||
end
|
||||
|
||||
def update_featured_tags!
|
||||
return unless @status.distributable?
|
||||
|
||||
added_tags = @current_tags - @previous_tags
|
||||
|
||||
unless added_tags.empty?
|
||||
@account.featured_tags.where(tag_id: added_tags.map(&:id)).each do |featured_tag|
|
||||
featured_tag.increment(@status.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
return unless status.distributable?
|
||||
removed_tags = @previous_tags - @current_tags
|
||||
|
||||
status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
|
||||
featured_tag.increment(status.created_at)
|
||||
unless removed_tags.empty?
|
||||
@account.featured_tags.where(tag_id: removed_tags.map(&:id)).each do |featured_tag|
|
||||
featured_tag.decrement(@status.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -8,13 +8,15 @@ class ReportService < BaseService
|
||||
@target_account = target_account
|
||||
@status_ids = options.delete(:status_ids) || []
|
||||
@comment = options.delete(:comment) || ''
|
||||
@category = options.delete(:category) || 'other'
|
||||
@rule_ids = options.delete(:rule_ids)
|
||||
@options = options
|
||||
|
||||
raise ActiveRecord::RecordNotFound if @target_account.suspended?
|
||||
|
||||
create_report!
|
||||
notify_staff!
|
||||
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||
forward_to_origin! if forward?
|
||||
|
||||
@report
|
||||
end
|
||||
@ -27,7 +29,9 @@ class ReportService < BaseService
|
||||
status_ids: @status_ids,
|
||||
comment: @comment,
|
||||
uri: @options[:uri],
|
||||
forwarded: ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||
forwarded: forward?,
|
||||
category: @category,
|
||||
rule_ids: @rule_ids
|
||||
)
|
||||
end
|
||||
|
||||
@ -48,6 +52,10 @@ class ReportService < BaseService
|
||||
)
|
||||
end
|
||||
|
||||
def forward?
|
||||
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||
end
|
||||
|
||||
def payload
|
||||
Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
|
||||
end
|
||||
|
151
app/services/update_status_service.rb
Normal file
151
app/services/update_status_service.rb
Normal file
@ -0,0 +1,151 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UpdateStatusService < BaseService
|
||||
include Redisable
|
||||
include LanguagesHelper
|
||||
|
||||
# @param [Status] status
|
||||
# @param [Integer] account_id
|
||||
# @param [Hash] options
|
||||
# @option options [Array<Integer>] :media_ids
|
||||
# @option options [Hash] :poll
|
||||
# @option options [String] :text
|
||||
# @option options [String] :spoiler_text
|
||||
# @option options [Boolean] :sensitive
|
||||
# @option options [String] :language
|
||||
def call(status, account_id, options = {})
|
||||
@status = status
|
||||
@options = options
|
||||
@account_id = account_id
|
||||
@media_attachments_changed = false
|
||||
@poll_changed = false
|
||||
|
||||
Status.transaction do
|
||||
create_previous_edit!
|
||||
update_media_attachments!
|
||||
update_poll!
|
||||
update_immediate_attributes!
|
||||
create_edit!
|
||||
end
|
||||
|
||||
queue_poll_notifications!
|
||||
reset_preview_card!
|
||||
update_metadata!
|
||||
broadcast_updates!
|
||||
|
||||
@status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_media_attachments!
|
||||
previous_media_attachments = @status.media_attachments.to_a
|
||||
next_media_attachments = validate_media!
|
||||
removed_media_attachments = previous_media_attachments - next_media_attachments
|
||||
added_media_attachments = next_media_attachments - previous_media_attachments
|
||||
|
||||
MediaAttachment.where(id: removed_media_attachments.map(&:id)).update_all(status_id: nil)
|
||||
MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id)
|
||||
|
||||
@status.media_attachments.reload
|
||||
@media_attachments_changed = true if removed_media_attachments.any? || added_media_attachments.any?
|
||||
end
|
||||
|
||||
def validate_media!
|
||||
return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present?
|
||||
|
||||
media_attachments = @status.account.media_attachments.where(status_id: [nil, @status.id]).where(scheduled_status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)).to_a
|
||||
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media_attachments.size > 1 && media_attachments.find(&:audio_or_video?)
|
||||
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if media_attachments.any?(&:not_processed?)
|
||||
|
||||
media_attachments
|
||||
end
|
||||
|
||||
def update_poll!
|
||||
previous_poll = @status.preloadable_poll
|
||||
@previous_expires_at = previous_poll&.expires_at
|
||||
|
||||
if @options[:poll].present?
|
||||
poll = previous_poll || @status.account.polls.new(status: @status, votes_count: 0)
|
||||
|
||||
# If for some reasons the options were changed, it invalidates all previous
|
||||
# votes, so we need to remove them
|
||||
@poll_changed = true if @options[:poll][:options] != poll.options || ActiveModel::Type::Boolean.new.cast(@options[:poll][:multiple]) != poll.multiple
|
||||
|
||||
poll.options = @options[:poll][:options]
|
||||
poll.hide_totals = @options[:poll][:hide_totals] || false
|
||||
poll.multiple = @options[:poll][:multiple] || false
|
||||
poll.expires_in = @options[:poll][:expires_in]
|
||||
poll.reset_votes! if @poll_changed
|
||||
poll.save!
|
||||
|
||||
@status.poll_id = poll.id
|
||||
elsif previous_poll.present?
|
||||
previous_poll.destroy
|
||||
@poll_changed = true
|
||||
@status.poll_id = nil
|
||||
end
|
||||
end
|
||||
|
||||
def update_immediate_attributes!
|
||||
@status.text = @options[:text].presence || @options.delete(:spoiler_text) || ''
|
||||
@status.spoiler_text = @options[:spoiler_text] || ''
|
||||
@status.sensitive = @options[:sensitive] || @options[:spoiler_text].present?
|
||||
@status.language = valid_locale_or_nil(@options[:language] || @status.language || @status.account.user&.preferred_posting_language || I18n.default_locale)
|
||||
@status.edited_at = Time.now.utc
|
||||
|
||||
@status.save!
|
||||
end
|
||||
|
||||
def reset_preview_card!
|
||||
return unless @status.text_previously_changed?
|
||||
|
||||
@status.preview_cards.clear
|
||||
LinkCrawlWorker.perform_async(@status.id)
|
||||
end
|
||||
|
||||
def update_metadata!
|
||||
ProcessHashtagsService.new.call(@status)
|
||||
ProcessMentionsService.new.call(@status)
|
||||
end
|
||||
|
||||
def broadcast_updates!
|
||||
DistributionWorker.perform_async(@status.id, { 'update' => true })
|
||||
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id)
|
||||
end
|
||||
|
||||
def queue_poll_notifications!
|
||||
poll = @status.preloadable_poll
|
||||
|
||||
# If the poll had no expiration date set but now has, or now has a sooner
|
||||
# expiration date, and people have voted, schedule a notification
|
||||
|
||||
return unless poll.present? && poll.expires_at.present? && poll.votes.exists?
|
||||
|
||||
PollExpirationNotifyWorker.remove_from_scheduled(poll.id) if @previous_expires_at.present? && @previous_expires_at > poll.expires_at
|
||||
PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
|
||||
end
|
||||
|
||||
def create_previous_edit!
|
||||
# We only need to create a previous edit when no previous edits exist, e.g.
|
||||
# when the status has never been edited. For other cases, we always create
|
||||
# an edit, so the step can be skipped
|
||||
|
||||
return if @status.edits.any?
|
||||
|
||||
@status.snapshot!(
|
||||
media_attachments_changed: false,
|
||||
at_time: @status.created_at
|
||||
)
|
||||
end
|
||||
|
||||
def create_edit!
|
||||
@status.snapshot!(
|
||||
media_attachments_changed: @media_attachments_changed || @poll_changed,
|
||||
account_id: @account_id
|
||||
)
|
||||
end
|
||||
end
|
29
app/workers/activitypub/status_update_distribution_worker.rb
Normal file
29
app/workers/activitypub/status_update_distribution_worker.rb
Normal file
@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWorker
|
||||
# Distribute an profile update to servers that might have a copy
|
||||
# of the account in question
|
||||
def perform(status_id, options = {})
|
||||
@options = options.with_indifferent_access
|
||||
@status = Status.find(status_id)
|
||||
@account = @status.account
|
||||
|
||||
distribute!
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def activity
|
||||
ActivityPub::ActivityPresenter.new(
|
||||
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join,
|
||||
type: 'Update',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(@status.account),
|
||||
published: @status.edited_at,
|
||||
to: ActivityPub::TagManager.instance.to(@status),
|
||||
cc: ActivityPub::TagManager.instance.cc(@status),
|
||||
virtual_object: @status
|
||||
)
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user