]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/actions/compose.js
Cancel outdated pending compose suggestions (#6838)
[mastodon.git] / app / javascript / mastodon / actions / compose.js
1 import api from '../api';
2 import { CancelToken } from 'axios';
3 import { throttle } from 'lodash';
4 import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
5 import { tagHistory } from '../settings';
6 import { useEmoji } from './emojis';
7
8 import {
9 updateTimeline,
10 refreshHomeTimeline,
11 refreshCommunityTimeline,
12 refreshPublicTimeline,
13 } from './timelines';
14
15 let cancelFetchComposeSuggestionsAccounts;
16
17 export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
18 export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
19 export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
20 export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
21 export const COMPOSE_REPLY = 'COMPOSE_REPLY';
22 export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
23 export const COMPOSE_MENTION = 'COMPOSE_MENTION';
24 export const COMPOSE_RESET = 'COMPOSE_RESET';
25 export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
26 export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
27 export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
28 export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
29 export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
30
31 export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
32 export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
33 export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
34 export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
35
36 export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
37
38 export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
39 export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
40
41 export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
42 export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
43 export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
44 export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
45 export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
46 export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
47
48 export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
49
50 export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
51 export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
52 export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
53
54 export function changeCompose(text) {
55 return {
56 type: COMPOSE_CHANGE,
57 text: text,
58 };
59 };
60
61 export function replyCompose(status, router) {
62 return (dispatch, getState) => {
63 dispatch({
64 type: COMPOSE_REPLY,
65 status: status,
66 });
67
68 if (!getState().getIn(['compose', 'mounted'])) {
69 router.push('/statuses/new');
70 }
71 };
72 };
73
74 export function cancelReplyCompose() {
75 return {
76 type: COMPOSE_REPLY_CANCEL,
77 };
78 };
79
80 export function resetCompose() {
81 return {
82 type: COMPOSE_RESET,
83 };
84 };
85
86 export function mentionCompose(account, router) {
87 return (dispatch, getState) => {
88 dispatch({
89 type: COMPOSE_MENTION,
90 account: account,
91 });
92
93 if (!getState().getIn(['compose', 'mounted'])) {
94 router.push('/statuses/new');
95 }
96 };
97 };
98
99 export function submitCompose() {
100 return function (dispatch, getState) {
101 const status = getState().getIn(['compose', 'text'], '');
102 const media = getState().getIn(['compose', 'media_attachments']);
103
104 if ((!status || !status.length) && media.size === 0) {
105 return;
106 }
107
108 dispatch(submitComposeRequest());
109
110 api(getState).post('/api/v1/statuses', {
111 status,
112 in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
113 media_ids: media.map(item => item.get('id')),
114 sensitive: getState().getIn(['compose', 'sensitive']),
115 spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
116 visibility: getState().getIn(['compose', 'privacy']),
117 }, {
118 headers: {
119 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
120 },
121 }).then(function (response) {
122 dispatch(insertIntoTagHistory(response.data.tags));
123 dispatch(submitComposeSuccess({ ...response.data }));
124
125 // To make the app more responsive, immediately get the status into the columns
126
127 const insertOrRefresh = (timelineId, refreshAction) => {
128 if (getState().getIn(['timelines', timelineId, 'online'])) {
129 dispatch(updateTimeline(timelineId, { ...response.data }));
130 } else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
131 dispatch(refreshAction());
132 }
133 };
134
135 insertOrRefresh('home', refreshHomeTimeline);
136
137 if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
138 insertOrRefresh('community', refreshCommunityTimeline);
139 insertOrRefresh('public', refreshPublicTimeline);
140 }
141 }).catch(function (error) {
142 dispatch(submitComposeFail(error));
143 });
144 };
145 };
146
147 export function submitComposeRequest() {
148 return {
149 type: COMPOSE_SUBMIT_REQUEST,
150 };
151 };
152
153 export function submitComposeSuccess(status) {
154 return {
155 type: COMPOSE_SUBMIT_SUCCESS,
156 status: status,
157 };
158 };
159
160 export function submitComposeFail(error) {
161 return {
162 type: COMPOSE_SUBMIT_FAIL,
163 error: error,
164 };
165 };
166
167 export function uploadCompose(files) {
168 return function (dispatch, getState) {
169 if (getState().getIn(['compose', 'media_attachments']).size > 3) {
170 return;
171 }
172
173 dispatch(uploadComposeRequest());
174
175 let data = new FormData();
176 data.append('file', files[0]);
177
178 api(getState).post('/api/v1/media', data, {
179 onUploadProgress: function (e) {
180 dispatch(uploadComposeProgress(e.loaded, e.total));
181 },
182 }).then(function (response) {
183 dispatch(uploadComposeSuccess(response.data));
184 }).catch(function (error) {
185 dispatch(uploadComposeFail(error));
186 });
187 };
188 };
189
190 export function changeUploadCompose(id, params) {
191 return (dispatch, getState) => {
192 dispatch(changeUploadComposeRequest());
193
194 api(getState).put(`/api/v1/media/${id}`, params).then(response => {
195 dispatch(changeUploadComposeSuccess(response.data));
196 }).catch(error => {
197 dispatch(changeUploadComposeFail(id, error));
198 });
199 };
200 };
201
202 export function changeUploadComposeRequest() {
203 return {
204 type: COMPOSE_UPLOAD_CHANGE_REQUEST,
205 skipLoading: true,
206 };
207 };
208 export function changeUploadComposeSuccess(media) {
209 return {
210 type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
211 media: media,
212 skipLoading: true,
213 };
214 };
215
216 export function changeUploadComposeFail(error) {
217 return {
218 type: COMPOSE_UPLOAD_CHANGE_FAIL,
219 error: error,
220 skipLoading: true,
221 };
222 };
223
224 export function uploadComposeRequest() {
225 return {
226 type: COMPOSE_UPLOAD_REQUEST,
227 skipLoading: true,
228 };
229 };
230
231 export function uploadComposeProgress(loaded, total) {
232 return {
233 type: COMPOSE_UPLOAD_PROGRESS,
234 loaded: loaded,
235 total: total,
236 };
237 };
238
239 export function uploadComposeSuccess(media) {
240 return {
241 type: COMPOSE_UPLOAD_SUCCESS,
242 media: media,
243 skipLoading: true,
244 };
245 };
246
247 export function uploadComposeFail(error) {
248 return {
249 type: COMPOSE_UPLOAD_FAIL,
250 error: error,
251 skipLoading: true,
252 };
253 };
254
255 export function undoUploadCompose(media_id) {
256 return {
257 type: COMPOSE_UPLOAD_UNDO,
258 media_id: media_id,
259 };
260 };
261
262 export function clearComposeSuggestions() {
263 if (cancelFetchComposeSuggestionsAccounts) {
264 cancelFetchComposeSuggestionsAccounts();
265 }
266 return {
267 type: COMPOSE_SUGGESTIONS_CLEAR,
268 };
269 };
270
271 const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
272 if (cancelFetchComposeSuggestionsAccounts) {
273 cancelFetchComposeSuggestionsAccounts();
274 }
275 api(getState).get('/api/v1/accounts/search', {
276 cancelToken: new CancelToken(cancel => {
277 cancelFetchComposeSuggestionsAccounts = cancel;
278 }),
279 params: {
280 q: token.slice(1),
281 resolve: false,
282 limit: 4,
283 },
284 }).then(response => {
285 dispatch(readyComposeSuggestionsAccounts(token, response.data));
286 });
287 }, 200, { leading: true, trailing: true });
288
289 const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
290 const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
291 dispatch(readyComposeSuggestionsEmojis(token, results));
292 };
293
294 const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
295 dispatch(updateSuggestionTags(token));
296 };
297
298 export function fetchComposeSuggestions(token) {
299 return (dispatch, getState) => {
300 switch (token[0]) {
301 case ':':
302 fetchComposeSuggestionsEmojis(dispatch, getState, token);
303 break;
304 case '#':
305 fetchComposeSuggestionsTags(dispatch, getState, token);
306 break;
307 default:
308 fetchComposeSuggestionsAccounts(dispatch, getState, token);
309 break;
310 }
311 };
312 };
313
314 export function readyComposeSuggestionsEmojis(token, emojis) {
315 return {
316 type: COMPOSE_SUGGESTIONS_READY,
317 token,
318 emojis,
319 };
320 };
321
322 export function readyComposeSuggestionsAccounts(token, accounts) {
323 return {
324 type: COMPOSE_SUGGESTIONS_READY,
325 token,
326 accounts,
327 };
328 };
329
330 export function selectComposeSuggestion(position, token, suggestion) {
331 return (dispatch, getState) => {
332 let completion, startPosition;
333
334 if (typeof suggestion === 'object' && suggestion.id) {
335 completion = suggestion.native || suggestion.colons;
336 startPosition = position - 1;
337
338 dispatch(useEmoji(suggestion));
339 } else if (suggestion[0] === '#') {
340 completion = suggestion;
341 startPosition = position - 1;
342 } else {
343 completion = getState().getIn(['accounts', suggestion, 'acct']);
344 startPosition = position;
345 }
346
347 dispatch({
348 type: COMPOSE_SUGGESTION_SELECT,
349 position: startPosition,
350 token,
351 completion,
352 });
353 };
354 };
355
356 export function updateSuggestionTags(token) {
357 return {
358 type: COMPOSE_SUGGESTION_TAGS_UPDATE,
359 token,
360 };
361 }
362
363 export function updateTagHistory(tags) {
364 return {
365 type: COMPOSE_TAG_HISTORY_UPDATE,
366 tags,
367 };
368 }
369
370 export function hydrateCompose() {
371 return (dispatch, getState) => {
372 const me = getState().getIn(['meta', 'me']);
373 const history = tagHistory.get(me);
374
375 if (history !== null) {
376 dispatch(updateTagHistory(history));
377 }
378 };
379 }
380
381 function insertIntoTagHistory(tags) {
382 return (dispatch, getState) => {
383 const state = getState();
384 const oldHistory = state.getIn(['compose', 'tagHistory']);
385 const me = state.getIn(['meta', 'me']);
386 const names = tags.map(({ name }) => name);
387 const intersectedOldHistory = oldHistory.filter(name => !names.includes(name));
388
389 names.push(...intersectedOldHistory.toJS());
390
391 const newHistory = names.slice(0, 1000);
392
393 tagHistory.set(me, newHistory);
394 dispatch(updateTagHistory(newHistory));
395 };
396 }
397
398 export function mountCompose() {
399 return {
400 type: COMPOSE_MOUNT,
401 };
402 };
403
404 export function unmountCompose() {
405 return {
406 type: COMPOSE_UNMOUNT,
407 };
408 };
409
410 export function changeComposeSensitivity() {
411 return {
412 type: COMPOSE_SENSITIVITY_CHANGE,
413 };
414 };
415
416 export function changeComposeSpoilerness() {
417 return {
418 type: COMPOSE_SPOILERNESS_CHANGE,
419 };
420 };
421
422 export function changeComposeSpoilerText(text) {
423 return {
424 type: COMPOSE_SPOILER_TEXT_CHANGE,
425 text,
426 };
427 };
428
429 export function changeComposeVisibility(value) {
430 return {
431 type: COMPOSE_VISIBILITY_CHANGE,
432 value,
433 };
434 };
435
436 export function insertEmojiCompose(position, emoji) {
437 return {
438 type: COMPOSE_EMOJI_INSERT,
439 position,
440 emoji,
441 };
442 };
443
444 export function changeComposing(value) {
445 return {
446 type: COMPOSE_COMPOSING_CHANGE,
447 value,
448 };
449 }
This page took 0.138808 seconds and 4 git commands to generate.