9 COMPOSE_SUBMIT_REQUEST
,
10 COMPOSE_SUBMIT_SUCCESS
,
12 COMPOSE_UPLOAD_REQUEST
,
13 COMPOSE_UPLOAD_SUCCESS
,
16 COMPOSE_UPLOAD_PROGRESS
,
17 COMPOSE_SUGGESTIONS_CLEAR
,
18 COMPOSE_SUGGESTIONS_READY
,
19 COMPOSE_SUGGESTION_SELECT
,
20 COMPOSE_SUGGESTION_TAGS_UPDATE
,
21 COMPOSE_TAG_HISTORY_UPDATE
,
22 COMPOSE_SENSITIVITY_CHANGE
,
23 COMPOSE_SPOILERNESS_CHANGE
,
24 COMPOSE_SPOILER_TEXT_CHANGE
,
25 COMPOSE_VISIBILITY_CHANGE
,
26 COMPOSE_COMPOSING_CHANGE
,
28 COMPOSE_UPLOAD_CHANGE_REQUEST
,
29 COMPOSE_UPLOAD_CHANGE_SUCCESS
,
30 COMPOSE_UPLOAD_CHANGE_FAIL
,
34 COMPOSE_POLL_OPTION_ADD
,
35 COMPOSE_POLL_OPTION_CHANGE
,
36 COMPOSE_POLL_OPTION_REMOVE
,
37 COMPOSE_POLL_SETTINGS_CHANGE
,
38 } from '../actions/compose';
39 import { TIMELINE_DELETE
} from '../actions/timelines';
40 import { STORE_HYDRATE
} from '../actions/store';
41 import { REDRAFT
} from '../actions/statuses';
42 import { Map as ImmutableMap
, List as ImmutableList
, OrderedSet as ImmutableOrderedSet
, fromJS
} from 'immutable';
43 import uuid
from '../uuid';
44 import { me
} from '../initial_state';
45 import { unescapeHTML
} from '../utils/html';
47 const initialState
= ImmutableMap({
60 is_changing_upload: false,
63 media_attachments: ImmutableList(),
65 suggestion_token: null,
66 suggestions: ImmutableList(),
67 default_privacy: 'public',
68 default_sensitive: false,
69 resetFileKey: Math
.floor((Math
.random() * 0x10000)),
71 tagHistory: ImmutableList(),
74 const initialPoll
= ImmutableMap({
75 options: ImmutableList(['', '']),
76 expires_in: 24 * 3600,
80 function statusToTextMentions(state
, status
) {
81 let set = ImmutableOrderedSet([]);
83 if (status
.getIn(['account', 'id']) !== me
) {
84 set = set.add(`@${status.getIn(['account', 'acct'])} `);
87 return set.union(status
.get('mentions').filterNot(mention
=> mention
.get('id') === me
).map(mention
=> `@${mention.get('acct')} `)).join('');
90 function clearAll(state
) {
91 return state
.withMutations(map
=> {
93 map
.set('spoiler', false);
94 map
.set('spoiler_text', '');
95 map
.set('is_submitting', false);
96 map
.set('is_changing_upload', false);
97 map
.set('in_reply_to', null);
98 map
.set('privacy', state
.get('default_privacy'));
99 map
.set('sensitive', false);
100 map
.update('media_attachments', list
=> list
.clear());
101 map
.set('poll', null);
102 map
.set('idempotencyKey', uuid());
106 function appendMedia(state
, media
) {
107 const prevSize
= state
.get('media_attachments').size
;
109 return state
.withMutations(map
=> {
110 map
.update('media_attachments', list
=> list
.push(media
));
111 map
.set('is_uploading', false);
112 map
.set('resetFileKey', Math
.floor((Math
.random() * 0x10000)));
113 map
.set('idempotencyKey', uuid());
115 if (prevSize
=== 0 && (state
.get('default_sensitive') || state
.get('spoiler'))) {
116 map
.set('sensitive', true);
121 function removeMedia(state
, mediaId
) {
122 const prevSize
= state
.get('media_attachments').size
;
124 return state
.withMutations(map
=> {
125 map
.update('media_attachments', list
=> list
.filterNot(item
=> item
.get('id') === mediaId
));
126 map
.set('idempotencyKey', uuid());
128 if (prevSize
=== 1) {
129 map
.set('sensitive', false);
134 const insertSuggestion
= (state
, position
, token
, completion
, path
) => {
135 return state
.withMutations(map
=> {
136 map
.updateIn(path
, oldText
=> `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
137 map
.set('suggestion_token', null);
138 map
.set('suggestions', ImmutableList());
139 if (path
.length
=== 1 && path
[0] === 'text') {
140 map
.set('focusDate', new Date());
141 map
.set('caretPosition', position
+ completion
.length
+ 1);
143 map
.set('idempotencyKey', uuid());
147 const updateSuggestionTags
= (state
, token
) => {
148 const prefix
= token
.slice(1);
151 suggestions: state
.get('tagHistory')
152 .filter(tag
=> tag
.toLowerCase().startsWith(prefix
.toLowerCase()))
154 .map(tag
=> '#' + tag
),
155 suggestion_token: token
,
159 const insertEmoji
= (state
, position
, emojiData
, needsSpace
) => {
160 const oldText
= state
.get('text');
161 const emoji
= needsSpace
? ' ' + emojiData
.native : emojiData
.native;
164 text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
165 focusDate: new Date(),
166 caretPosition: position
+ emoji
.length
+ 1,
167 idempotencyKey: uuid(),
171 const privacyPreference
= (a
, b
) => {
172 const order
= ['public', 'unlisted', 'private', 'direct'];
173 return order
[Math
.max(order
.indexOf(a
), order
.indexOf(b
), 0)];
176 const hydrate
= (state
, hydratedState
) => {
177 state
= clearAll(state
.merge(hydratedState
));
179 if (hydratedState
.has('text')) {
180 state
= state
.set('text', hydratedState
.get('text'));
186 const domParser
= new DOMParser();
188 const expandMentions
= status
=> {
189 const fragment
= domParser
.parseFromString(status
.get('content'), 'text/html').documentElement
;
191 status
.get('mentions').forEach(mention
=> {
192 fragment
.querySelector(`a[href="${mention.get('url')}"]`).textContent
= `@${mention.get('acct')}`;
195 return fragment
.innerHTML
;
198 export default function compose(state
= initialState
, action
) {
199 switch(action
.type
) {
201 return hydrate(state
, action
.state
.get('compose'));
203 return state
.set('mounted', state
.get('mounted') + 1);
204 case COMPOSE_UNMOUNT:
206 .set('mounted', Math
.max(state
.get('mounted') - 1, 0))
207 .set('is_composing', false);
208 case COMPOSE_SENSITIVITY_CHANGE:
209 return state
.withMutations(map
=> {
210 if (!state
.get('spoiler')) {
211 map
.set('sensitive', !state
.get('sensitive'));
214 map
.set('idempotencyKey', uuid());
216 case COMPOSE_SPOILERNESS_CHANGE:
217 return state
.withMutations(map
=> {
218 map
.set('spoiler_text', '');
219 map
.set('spoiler', !state
.get('spoiler'));
220 map
.set('idempotencyKey', uuid());
222 if (!state
.get('sensitive') && state
.get('media_attachments').size
>= 1) {
223 map
.set('sensitive', true);
226 case COMPOSE_SPOILER_TEXT_CHANGE:
228 .set('spoiler_text', action
.text
)
229 .set('idempotencyKey', uuid());
230 case COMPOSE_VISIBILITY_CHANGE:
232 .set('privacy', action
.value
)
233 .set('idempotencyKey', uuid());
236 .set('text', action
.text
)
237 .set('idempotencyKey', uuid());
238 case COMPOSE_COMPOSING_CHANGE:
239 return state
.set('is_composing', action
.value
);
241 return state
.withMutations(map
=> {
242 map
.set('in_reply_to', action
.status
.get('id'));
243 map
.set('text', statusToTextMentions(state
, action
.status
));
244 map
.set('privacy', privacyPreference(action
.status
.get('visibility'), state
.get('default_privacy')));
245 map
.set('focusDate', new Date());
246 map
.set('caretPosition', null);
247 map
.set('preselectDate', new Date());
248 map
.set('idempotencyKey', uuid());
250 if (action
.status
.get('spoiler_text').length
> 0) {
251 map
.set('spoiler', true);
252 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
254 map
.set('spoiler', false);
255 map
.set('spoiler_text', '');
258 case COMPOSE_REPLY_CANCEL:
260 return state
.withMutations(map
=> {
261 map
.set('in_reply_to', null);
263 map
.set('spoiler', false);
264 map
.set('spoiler_text', '');
265 map
.set('privacy', state
.get('default_privacy'));
266 map
.set('poll', null);
267 map
.set('idempotencyKey', uuid());
269 case COMPOSE_SUBMIT_REQUEST:
270 return state
.set('is_submitting', true);
271 case COMPOSE_UPLOAD_CHANGE_REQUEST:
272 return state
.set('is_changing_upload', true);
273 case COMPOSE_SUBMIT_SUCCESS:
274 return clearAll(state
);
275 case COMPOSE_SUBMIT_FAIL:
276 return state
.set('is_submitting', false);
277 case COMPOSE_UPLOAD_CHANGE_FAIL:
278 return state
.set('is_changing_upload', false);
279 case COMPOSE_UPLOAD_REQUEST:
280 return state
.set('is_uploading', true);
281 case COMPOSE_UPLOAD_SUCCESS:
282 return appendMedia(state
, fromJS(action
.media
));
283 case COMPOSE_UPLOAD_FAIL:
284 return state
.set('is_uploading', false);
285 case COMPOSE_UPLOAD_UNDO:
286 return removeMedia(state
, action
.media_id
);
287 case COMPOSE_UPLOAD_PROGRESS:
288 return state
.set('progress', Math
.round((action
.loaded
/ action
.total
) * 100));
289 case COMPOSE_MENTION:
290 return state
.withMutations(map
=> {
291 map
.update('text', text
=> [text
.trim(), `@${action.account.get('acct')} `].filter((str
) => str
.length
!== 0).join(' '));
292 map
.set('focusDate', new Date());
293 map
.set('caretPosition', null);
294 map
.set('idempotencyKey', uuid());
297 return state
.withMutations(map
=> {
298 map
.update('text', text
=> [text
.trim(), `@${action.account.get('acct')} `].filter((str
) => str
.length
!== 0).join(' '));
299 map
.set('privacy', 'direct');
300 map
.set('focusDate', new Date());
301 map
.set('caretPosition', null);
302 map
.set('idempotencyKey', uuid());
304 case COMPOSE_SUGGESTIONS_CLEAR:
305 return state
.update('suggestions', ImmutableList(), list
=> list
.clear()).set('suggestion_token', null);
306 case COMPOSE_SUGGESTIONS_READY:
307 return state
.set('suggestions', ImmutableList(action
.accounts
? action
.accounts
.map(item
=> item
.id
) : action
.emojis
)).set('suggestion_token', action
.token
);
308 case COMPOSE_SUGGESTION_SELECT:
309 return insertSuggestion(state
, action
.position
, action
.token
, action
.completion
, action
.path
);
310 case COMPOSE_SUGGESTION_TAGS_UPDATE:
311 return updateSuggestionTags(state
, action
.token
);
312 case COMPOSE_TAG_HISTORY_UPDATE:
313 return state
.set('tagHistory', fromJS(action
.tags
));
314 case TIMELINE_DELETE:
315 if (action
.id
=== state
.get('in_reply_to')) {
316 return state
.set('in_reply_to', null);
320 case COMPOSE_EMOJI_INSERT:
321 return insertEmoji(state
, action
.position
, action
.emoji
, action
.needsSpace
);
322 case COMPOSE_UPLOAD_CHANGE_SUCCESS:
324 .set('is_changing_upload', false)
325 .update('media_attachments', list
=> list
.map(item
=> {
326 if (item
.get('id') === action
.media
.id
) {
327 return fromJS(action
.media
);
333 return state
.withMutations(map
=> {
334 map
.set('text', action
.raw_content
|| unescapeHTML(expandMentions(action
.status
)));
335 map
.set('in_reply_to', action
.status
.get('in_reply_to_id'));
336 map
.set('privacy', action
.status
.get('visibility'));
337 map
.set('media_attachments', action
.status
.get('media_attachments'));
338 map
.set('focusDate', new Date());
339 map
.set('caretPosition', null);
340 map
.set('idempotencyKey', uuid());
342 if (action
.status
.get('spoiler_text').length
> 0) {
343 map
.set('spoiler', true);
344 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
346 map
.set('spoiler', false);
347 map
.set('spoiler_text', '');
350 if (action
.status
.get('poll')) {
351 map
.set('poll', ImmutableMap({
352 options: action
.status
.getIn(['poll', 'options']).map(x
=> x
.get('title')),
353 multiple: action
.status
.getIn(['poll', 'multiple']),
354 expires_in: 24 * 3600,
358 case COMPOSE_POLL_ADD:
359 return state
.set('poll', initialPoll
);
360 case COMPOSE_POLL_REMOVE:
361 return state
.set('poll', null);
362 case COMPOSE_POLL_OPTION_ADD:
363 return state
.updateIn(['poll', 'options'], options
=> options
.push(action
.title
));
364 case COMPOSE_POLL_OPTION_CHANGE:
365 return state
.setIn(['poll', 'options', action
.index
], action
.title
);
366 case COMPOSE_POLL_OPTION_REMOVE:
367 return state
.updateIn(['poll', 'options'], options
=> options
.delete(action
.index
));
368 case COMPOSE_POLL_SETTINGS_CHANGE:
369 return state
.update('poll', poll
=> poll
.set('expires_in', action
.expiresIn
).set('multiple', action
.isMultiple
));