Skip to content

Commit 0cf16d1

Browse files
committed
Add editing for published statuses
1 parent 10188c7 commit 0cf16d1

26 files changed

Lines changed: 864 additions & 132 deletions

app/controllers/api/v1/media_controller.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ def show
2020
end
2121

2222
def update
23-
@media_attachment.update!(media_attachment_params)
23+
@media_attachment.update!(updateable_media_attachment_params)
24+
25+
# If the media attachment being updated is attached to a published
26+
# status, then the changes need to be recorded and distributed along
27+
# with the status, but it may be that the status is going to be updated
28+
# along with the media attachment, so we need to debounce
29+
if @media_attachment.status_id.present? && @media_attachment.significantly_changed?
30+
PublishMediaAttachmentUpdateWorker.perform_in(5.minutes, @media_attachment.id, @media_attachment.updated_at)
31+
end
32+
2433
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
2534
end
2635

@@ -31,7 +40,7 @@ def status_code_for_media_attachment
3140
end
3241

3342
def set_media_attachment
34-
@media_attachment = current_account.media_attachments.unattached.find(params[:id])
43+
@media_attachment = current_account.media_attachments.find(params[:id])
3544
end
3645

3746
def check_processing
@@ -42,6 +51,10 @@ def media_attachment_params
4251
params.permit(:file, :thumbnail, :description, :focus)
4352
end
4453

54+
def updateable_media_attachment_params
55+
params.permit(:thumbnail, :description, :focus)
56+
end
57+
4558
def file_type_error
4659
{ error: 'File type of uploaded media could not be verified' }
4760
end

app/controllers/api/v1/statuses_controller.rb

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
class Api::V1::StatusesController < Api::BaseController
44
include Authorization
55

6-
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
7-
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
6+
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
7+
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
88
before_action :require_user!, except: [:show, :context]
99
before_action :set_status, only: [:show, :context]
1010
before_action :set_thread, only: [:create]
@@ -35,24 +35,44 @@ def context
3535
end
3636

3737
def create
38-
@status = PostStatusService.new.call(current_user.account,
39-
text: status_params[:status],
40-
thread: @thread,
41-
media_ids: status_params[:media_ids],
42-
sensitive: status_params[:sensitive],
43-
spoiler_text: status_params[:spoiler_text],
44-
visibility: status_params[:visibility],
45-
scheduled_at: status_params[:scheduled_at],
46-
application: doorkeeper_token.application,
47-
poll: status_params[:poll],
48-
idempotency: request.headers['Idempotency-Key'],
49-
with_rate_limit: true)
38+
@status = PostStatusService.new.call(
39+
current_user.account,
40+
text: status_params[:status],
41+
thread: @thread,
42+
media_ids: status_params[:media_ids],
43+
sensitive: status_params[:sensitive],
44+
spoiler_text: status_params[:spoiler_text],
45+
visibility: status_params[:visibility],
46+
language: status_params[:language],
47+
scheduled_at: status_params[:scheduled_at],
48+
application: doorkeeper_token.application,
49+
poll: status_params[:poll],
50+
idempotency: request.headers['Idempotency-Key'],
51+
with_rate_limit: true
52+
)
5053

5154
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
5255
end
5356

57+
def update
58+
@status = Status.where(account: current_account).find(params[:id])
59+
authorize @status, :update?
60+
61+
UpdateStatusService.new.call(
62+
@status,
63+
current_account.id,
64+
text: status_params[:status],
65+
media_ids: status_params[:media_ids],
66+
sensitive: status_params[:sensitive],
67+
spoiler_text: status_params[:spoiler_text],
68+
poll: status_params[:poll]
69+
)
70+
71+
render json: @status, serializer: REST::StatusSerializer
72+
end
73+
5474
def destroy
55-
@status = Status.where(account_id: current_user.account).find(params[:id])
75+
@status = Status.where(account: current_account).find(params[:id])
5676
authorize @status, :destroy?
5777

5878
@status.discard
@@ -84,6 +104,7 @@ def status_params
84104
:sensitive,
85105
:spoiler_text,
86106
:visibility,
107+
:language,
87108
:scheduled_at,
88109
media_ids: [],
89110
poll: [

app/javascript/mastodon/actions/compose.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
7070
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
7171
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
7272

73+
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
74+
7375
const messages = defineMessages({
7476
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
7577
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@@ -83,6 +85,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
8385
}
8486
};
8587

88+
export function setComposeToStatus(status, text, spoiler_text) {
89+
return{
90+
type: COMPOSE_SET_STATUS,
91+
status,
92+
text,
93+
spoiler_text,
94+
};
95+
};
96+
8697
export function changeCompose(text) {
8798
return {
8899
type: COMPOSE_CHANGE,
@@ -137,24 +148,28 @@ export function directCompose(account, routerHistory) {
137148

138149
export function submitCompose(routerHistory) {
139150
return function (dispatch, getState) {
140-
const status = getState().getIn(['compose', 'text'], '');
141-
const media = getState().getIn(['compose', 'media_attachments']);
151+
const status = getState().getIn(['compose', 'text'], '');
152+
const media = getState().getIn(['compose', 'media_attachments']);
153+
const statusId = getState().getIn(['compose', 'id'], null);
142154

143155
if ((!status || !status.length) && media.size === 0) {
144156
return;
145157
}
146158

147159
dispatch(submitComposeRequest());
148160

149-
api(getState).post('/api/v1/statuses', {
150-
status,
151-
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
152-
media_ids: media.map(item => item.get('id')),
153-
sensitive: getState().getIn(['compose', 'sensitive']),
154-
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
155-
visibility: getState().getIn(['compose', 'privacy']),
156-
poll: getState().getIn(['compose', 'poll'], null),
157-
}, {
161+
api(getState).request({
162+
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
163+
method: statusId === null ? 'post' : 'put',
164+
data: {
165+
status,
166+
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
167+
media_ids: media.map(item => item.get('id')),
168+
sensitive: getState().getIn(['compose', 'sensitive']),
169+
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
170+
visibility: getState().getIn(['compose', 'privacy']),
171+
poll: getState().getIn(['compose', 'poll'], null),
172+
},
158173
headers: {
159174
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
160175
},
@@ -176,11 +191,11 @@ export function submitCompose(routerHistory) {
176191
}
177192
};
178193

179-
if (response.data.visibility !== 'direct') {
194+
if (statusId === null && response.data.visibility !== 'direct') {
180195
insertIfOnline('home');
181196
}
182197

183-
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
198+
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
184199
insertIfOnline('community');
185200
insertIfOnline('public');
186201
insertIfOnline(`account:${response.data.account.id}`);

app/javascript/mastodon/actions/statuses.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import api from '../api';
22

33
import { deleteFromTimelines } from './timelines';
44
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
5-
import { ensureComposeIsVisible } from './compose';
5+
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
66

77
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
88
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@@ -30,6 +30,10 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
3030

3131
export const REDRAFT = 'REDRAFT';
3232

33+
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
34+
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
35+
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
36+
3337
export function fetchStatusRequest(id, skipLoading) {
3438
return {
3539
type: STATUS_FETCH_REQUEST,
@@ -84,25 +88,45 @@ export function redraft(status, raw_text) {
8488
};
8589
};
8690

87-
export function deleteStatus(id, routerHistory, withRedraft = false) {
88-
return (dispatch, getState) => {
89-
let status = getState().getIn(['statuses', id]);
91+
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
92+
let status = getState().getIn(['statuses', id]);
9093

91-
if (status.get('poll')) {
92-
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
93-
}
94+
if (status.get('poll')) {
95+
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
96+
}
97+
98+
dispatch(fetchStatusSourceRequest());
99+
100+
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
101+
dispatch(fetchStatusSourceSuccess());
102+
ensureComposeIsVisible(getState, routerHistory);
103+
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
104+
}).catch(error => {
105+
dispatch(fetchStatusSourceFail(error));
106+
});
107+
};
94108

109+
export const fetchStatusSourceRequest = () => ({
110+
type: STATUS_FETCH_SOURCE_REQUEST,
111+
});
112+
113+
export const fetchStatusSourceSuccess = () => ({
114+
type: STATUS_FETCH_SOURCE_SUCCESS,
115+
});
116+
117+
export const fetchStatusSourceFail = error => ({
118+
type: STATUS_FETCH_SOURCE_FAIL,
119+
error,
120+
});
121+
122+
export function deleteStatus(id) {
123+
return (dispatch, getState) => {
95124
dispatch(deleteStatusRequest(id));
96125

97126
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
98127
dispatch(deleteStatusSuccess(id));
99128
dispatch(deleteFromTimelines(id));
100129
dispatch(importFetchedAccount(response.data.account));
101-
102-
if (withRedraft) {
103-
dispatch(redraft(status, response.data.text));
104-
ensureComposeIsVisible(getState, routerHistory);
105-
}
106130
}).catch(error => {
107131
dispatch(deleteStatusFail(id, error));
108132
});

app/javascript/mastodon/components/status_action_bar.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import classNames from 'classnames';
1111

1212
const messages = defineMessages({
1313
delete: { id: 'status.delete', defaultMessage: 'Delete' },
14-
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
14+
edit: { id: 'status.edit', defaultMessage: 'Edit' },
1515
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
1616
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
1717
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@@ -133,8 +133,8 @@ class StatusActionBar extends ImmutablePureComponent {
133133
this.props.onDelete(this.props.status, this.context.router.history);
134134
}
135135

136-
handleRedraftClick = () => {
137-
this.props.onDelete(this.props.status, this.context.router.history, true);
136+
handleEditClick = () => {
137+
this.props.onEdit(this.props.status, this.context.router.history);
138138
}
139139

140140
handlePinClick = () => {
@@ -255,8 +255,8 @@ class StatusActionBar extends ImmutablePureComponent {
255255
}
256256

257257
if (writtenByMe) {
258+
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
258259
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
259-
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
260260
} else {
261261
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
262262
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });

app/javascript/mastodon/containers/status_container.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
hideStatus,
2525
revealStatus,
2626
toggleStatusCollapse,
27+
editStatus,
2728
} from '../actions/statuses';
2829
import {
2930
unmuteAccount,
@@ -130,18 +131,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
130131
}));
131132
},
132133

133-
onDelete (status, history, withRedraft = false) {
134+
onDelete (status, history) {
134135
if (!deleteModal) {
135-
dispatch(deleteStatus(status.get('id'), history, withRedraft));
136+
dispatch(deleteStatus(status.get('id'), history));
136137
} else {
137138
dispatch(openModal('CONFIRM', {
138-
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
139-
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
140-
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
139+
message: intl.formatMessage(messages.deleteMessage),
140+
confirm: intl.formatMessage(messages.deleteConfirm),
141+
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
141142
}));
142143
}
143144
},
144145

146+
onEdit (status, history) {
147+
dispatch(editStatus(status.get('id'), history));
148+
},
149+
145150
onDirect (account, router) {
146151
dispatch(directCompose(account, router));
147152
},

app/javascript/mastodon/features/compose/components/compose_form.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const messages = defineMessages({
2828
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
2929
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
3030
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
31+
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
3132
});
3233

3334
export default @injectIntl
@@ -49,6 +50,7 @@ class ComposeForm extends ImmutablePureComponent {
4950
preselectDate: PropTypes.instanceOf(Date),
5051
isSubmitting: PropTypes.bool,
5152
isChangingUpload: PropTypes.bool,
53+
isEditing: PropTypes.bool,
5254
isUploading: PropTypes.bool,
5355
onChange: PropTypes.func.isRequired,
5456
onSubmit: PropTypes.func.isRequired,
@@ -199,7 +201,9 @@ class ComposeForm extends ImmutablePureComponent {
199201
const disabled = this.props.isSubmitting;
200202
let publishText = '';
201203

202-
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
204+
if (this.props.isEditing) {
205+
publishText = intl.formatMessage(messages.saveChanges);
206+
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
203207
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
204208
} else {
205209
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);

app/javascript/mastodon/features/compose/containers/reply_indicator_container.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ import ReplyIndicator from '../components/reply_indicator';
66
const makeMapStateToProps = () => {
77
const getStatus = makeGetStatus();
88

9-
const mapStateToProps = state => ({
10-
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
11-
});
9+
const mapStateToProps = state => {
10+
let statusId = state.getIn(['compose', 'id'], null);
11+
let editing = true;
12+
13+
if (statusId === null) {
14+
statusId = state.getIn(['compose', 'in_reply_to']);
15+
editing = false;
16+
}
17+
18+
return {
19+
status: getStatus(state, { id: statusId }),
20+
editing,
21+
};
22+
};
1223

1324
return mapStateToProps;
1425
};

0 commit comments

Comments
 (0)