]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/actions/compose.js
Emoji and Hashtag autocomplete
[mastodon.git] / app / javascript / mastodon / actions / compose.js
1 import api from '../api';
2 import emojione from 'emojione';
3
4 import {
5 updateTimeline,
6 refreshHomeTimeline,
7 refreshCommunityTimeline,
8 refreshPublicTimeline,
9 } from './timelines';
10
11 export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
12 export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
13 export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
14 export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
15 export const COMPOSE_REPLY = 'COMPOSE_REPLY';
16 export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
17 export const COMPOSE_MENTION = 'COMPOSE_MENTION';
18 export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
19 export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
20 export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
21 export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
22 export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
23
24 export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
25 export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
26 export const COMPOSE_SUGGESTIONS_READY_TXT = 'COMPOSE_SUGGESTIONS_READY_TXT';
27 export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
28
29 export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
30 export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
31
32 export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
33 export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
34 export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
35 export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
36 export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
37 export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
38 export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
39
40 export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
41
42 export function changeCompose(text) {
43 return {
44 type: COMPOSE_CHANGE,
45 text: text,
46 };
47 };
48
49 export function replyCompose(status, router) {
50 return (dispatch, getState) => {
51 dispatch({
52 type: COMPOSE_REPLY,
53 status: status,
54 });
55
56 if (!getState().getIn(['compose', 'mounted'])) {
57 router.push('/statuses/new');
58 }
59 };
60 };
61
62 export function cancelReplyCompose() {
63 return {
64 type: COMPOSE_REPLY_CANCEL,
65 };
66 };
67
68 export function mentionCompose(account, router) {
69 return (dispatch, getState) => {
70 dispatch({
71 type: COMPOSE_MENTION,
72 account: account,
73 });
74
75 if (!getState().getIn(['compose', 'mounted'])) {
76 router.push('/statuses/new');
77 }
78 };
79 };
80
81 export function submitCompose() {
82 return function (dispatch, getState) {
83 let status = getState().getIn(['compose', 'text'], '');
84
85 if (!status || !status.length) {
86 return;
87 }
88
89 dispatch(submitComposeRequest());
90 if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
91 status = status + ' 👁️';
92 }
93 api(getState).post('/api/v1/statuses', {
94 status,
95 in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
96 media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
97 sensitive: getState().getIn(['compose', 'sensitive']),
98 spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
99 visibility: getState().getIn(['compose', 'privacy']),
100 }, {
101 headers: {
102 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
103 },
104 }).then(function (response) {
105 dispatch(submitComposeSuccess({ ...response.data }));
106
107 // To make the app more responsive, immediately get the status into the columns
108
109 const insertOrRefresh = (timelineId, refreshAction) => {
110 if (getState().getIn(['timelines', timelineId, 'online'])) {
111 dispatch(updateTimeline(timelineId, { ...response.data }));
112 } else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
113 dispatch(refreshAction());
114 }
115 };
116
117 insertOrRefresh('home', refreshHomeTimeline);
118
119 if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
120 insertOrRefresh('community', refreshCommunityTimeline);
121 insertOrRefresh('public', refreshPublicTimeline);
122 }
123 }).catch(function (error) {
124 dispatch(submitComposeFail(error));
125 });
126 };
127 };
128
129 export function submitComposeRequest() {
130 return {
131 type: COMPOSE_SUBMIT_REQUEST,
132 };
133 };
134
135 export function submitComposeSuccess(status) {
136 return {
137 type: COMPOSE_SUBMIT_SUCCESS,
138 status: status,
139 };
140 };
141
142 export function submitComposeFail(error) {
143 return {
144 type: COMPOSE_SUBMIT_FAIL,
145 error: error,
146 };
147 };
148
149 export function uploadCompose(files) {
150 return function (dispatch, getState) {
151 if (getState().getIn(['compose', 'media_attachments']).size > 3) {
152 return;
153 }
154
155 dispatch(uploadComposeRequest());
156
157 let data = new FormData();
158 data.append('file', files[0]);
159
160 api(getState).post('/api/v1/media', data, {
161 onUploadProgress: function (e) {
162 dispatch(uploadComposeProgress(e.loaded, e.total));
163 },
164 }).then(function (response) {
165 dispatch(uploadComposeSuccess(response.data));
166 }).catch(function (error) {
167 dispatch(uploadComposeFail(error));
168 });
169 };
170 };
171
172 export function uploadComposeRequest() {
173 return {
174 type: COMPOSE_UPLOAD_REQUEST,
175 skipLoading: true,
176 };
177 };
178
179 export function uploadComposeProgress(loaded, total) {
180 return {
181 type: COMPOSE_UPLOAD_PROGRESS,
182 loaded: loaded,
183 total: total,
184 };
185 };
186
187 export function uploadComposeSuccess(media) {
188 return {
189 type: COMPOSE_UPLOAD_SUCCESS,
190 media: media,
191 skipLoading: true,
192 };
193 };
194
195 export function uploadComposeFail(error) {
196 return {
197 type: COMPOSE_UPLOAD_FAIL,
198 error: error,
199 skipLoading: true,
200 };
201 };
202
203 export function undoUploadCompose(media_id) {
204 return {
205 type: COMPOSE_UPLOAD_UNDO,
206 media_id: media_id,
207 };
208 };
209
210 export function clearComposeSuggestions() {
211 return {
212 type: COMPOSE_SUGGESTIONS_CLEAR,
213 };
214 };
215
216 export function fetchComposeSuggestions(token) {
217 let leading = token[0];
218
219 if (leading === '@') {
220 // handle search
221 return (dispatch, getState) => {
222 api(getState).get('/api/v1/accounts/search', {
223 params: {
224 q: token.slice(1), // remove the '@'
225 resolve: false,
226 limit: 4,
227 },
228 }).then(response => {
229 dispatch(readyComposeSuggestions(token, response.data));
230 });
231 };
232 } else if (leading === ':') {
233 // mojos
234 let allShortcodes = Object.keys(emojione.emojioneList);
235 // TODO when we have custom emojons merged, add theme to this shortcode list
236 return (dispatch) => {
237 dispatch(readyComposeSuggestionsTxt(token, allShortcodes.filter((sc) => {
238 return sc.indexOf(token) === 0;
239 })));
240 };
241 } else {
242 // hashtag
243 return (dispatch, getState) => {
244 api(getState).get('/api/v1/search', {
245 params: {
246 q: token,
247 resolve: true,
248 },
249 }).then(response => {
250 dispatch(readyComposeSuggestionsTxt(token, response.data.hashtags.map((ht) => `#${ht}`)));
251 });
252 };
253 }
254 };
255
256 export function readyComposeSuggestions(token, accounts) {
257 return {
258 type: COMPOSE_SUGGESTIONS_READY,
259 token,
260 accounts,
261 };
262 };
263
264 export function readyComposeSuggestionsTxt(token, items) {
265 return {
266 type: COMPOSE_SUGGESTIONS_READY_TXT,
267 token,
268 items,
269 };
270 };
271
272 export function selectComposeSuggestion(position, token, accountId) {
273 return (dispatch, getState) => {
274 const completion = (typeof accountId === 'string') ?
275 accountId.slice(1) : // text suggestion: discard the leading : or # - the replacing code replaces only what follows
276 getState().getIn(['accounts', accountId, 'acct']);
277
278 dispatch({
279 type: COMPOSE_SUGGESTION_SELECT,
280 position,
281 token,
282 completion,
283 });
284 };
285 };
286
287 export function mountCompose() {
288 return {
289 type: COMPOSE_MOUNT,
290 };
291 };
292
293 export function unmountCompose() {
294 return {
295 type: COMPOSE_UNMOUNT,
296 };
297 };
298
299 export function toggleComposeAdvancedOption(option) {
300 return {
301 type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
302 option: option,
303 };
304 }
305
306 export function changeComposeSensitivity() {
307 return {
308 type: COMPOSE_SENSITIVITY_CHANGE,
309 };
310 };
311
312 export function changeComposeSpoilerness() {
313 return {
314 type: COMPOSE_SPOILERNESS_CHANGE,
315 };
316 };
317
318 export function changeComposeSpoilerText(text) {
319 return {
320 type: COMPOSE_SPOILER_TEXT_CHANGE,
321 text,
322 };
323 };
324
325 export function changeComposeVisibility(value) {
326 return {
327 type: COMPOSE_VISIBILITY_CHANGE,
328 value,
329 };
330 };
331
332 export function insertEmojiCompose(position, emoji) {
333 return {
334 type: COMPOSE_EMOJI_INSERT,
335 position,
336 emoji,
337 };
338 };
339
340 export function changeComposing(value) {
341 return {
342 type: COMPOSE_COMPOSING_CHANGE,
343 value,
344 };
345 }
This page took 0.106944 seconds and 4 git commands to generate.