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
,
32 } from '../actions/compose';
33 import { TIMELINE_DELETE
} from '../actions/timelines';
34 import { STORE_HYDRATE
} from '../actions/store';
35 import { REDRAFT
} from '../actions/statuses';
36 import { Map as ImmutableMap
, List as ImmutableList
, OrderedSet as ImmutableOrderedSet
, fromJS
} from 'immutable';
37 import uuid
from '../uuid';
38 import { me
} from '../initial_state';
39 import { unescapeHTML
} from '../utils/html';
41 const initialState
= ImmutableMap({
56 media_attachments: ImmutableList(),
57 suggestion_token: null,
58 suggestions: ImmutableList(),
59 default_privacy: 'public',
60 default_sensitive: false,
61 resetFileKey: Math
.floor((Math
.random() * 0x10000)),
63 tagHistory: ImmutableList(),
66 function statusToTextMentions(state
, status
) {
67 let set = ImmutableOrderedSet([]);
69 if (status
.getIn(['account', 'id']) !== me
) {
70 set = set.add(`@${status.getIn(['account', 'acct'])} `);
73 return set.union(status
.get('mentions').filterNot(mention
=> mention
.get('id') === me
).map(mention
=> `@${mention.get('acct')} `)).join('');
76 function clearAll(state
) {
77 return state
.withMutations(map
=> {
79 map
.set('spoiler', false);
80 map
.set('spoiler_text', '');
81 map
.set('is_submitting', false);
82 map
.set('in_reply_to', null);
83 map
.set('privacy', state
.get('default_privacy'));
84 map
.set('sensitive', false);
85 map
.update('media_attachments', list
=> list
.clear());
86 map
.set('idempotencyKey', uuid());
90 function appendMedia(state
, media
) {
91 const prevSize
= state
.get('media_attachments').size
;
93 return state
.withMutations(map
=> {
94 map
.update('media_attachments', list
=> list
.push(media
));
95 map
.set('is_uploading', false);
96 map
.set('resetFileKey', Math
.floor((Math
.random() * 0x10000)));
97 map
.set('idempotencyKey', uuid());
99 if (prevSize
=== 0 && (state
.get('default_sensitive') || state
.get('spoiler'))) {
100 map
.set('sensitive', true);
105 function removeMedia(state
, mediaId
) {
106 const prevSize
= state
.get('media_attachments').size
;
108 return state
.withMutations(map
=> {
109 map
.update('media_attachments', list
=> list
.filterNot(item
=> item
.get('id') === mediaId
));
110 map
.set('idempotencyKey', uuid());
112 if (prevSize
=== 1) {
113 map
.set('sensitive', false);
118 const insertSuggestion
= (state
, position
, token
, completion
) => {
119 return state
.withMutations(map
=> {
120 map
.update('text', oldText
=> `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
121 map
.set('suggestion_token', null);
122 map
.update('suggestions', ImmutableList(), list
=> list
.clear());
123 map
.set('focusDate', new Date());
124 map
.set('caretPosition', position
+ completion
.length
+ 1);
125 map
.set('idempotencyKey', uuid());
129 const updateSuggestionTags
= (state
, token
) => {
130 const prefix
= token
.slice(1);
133 suggestions: state
.get('tagHistory')
134 .filter(tag
=> tag
.toLowerCase().startsWith(prefix
.toLowerCase()))
136 .map(tag
=> '#' + tag
),
137 suggestion_token: token
,
141 const insertEmoji
= (state
, position
, emojiData
, needsSpace
) => {
142 const oldText
= state
.get('text');
143 const emoji
= needsSpace
? ' ' + emojiData
.native : emojiData
.native;
146 text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
147 focusDate: new Date(),
148 caretPosition: position
+ emoji
.length
+ 1,
149 idempotencyKey: uuid(),
153 const privacyPreference
= (a
, b
) => {
154 const order
= ['public', 'unlisted', 'private', 'direct'];
155 return order
[Math
.max(order
.indexOf(a
), order
.indexOf(b
), 0)];
158 const hydrate
= (state
, hydratedState
) => {
159 state
= clearAll(state
.merge(hydratedState
));
161 if (hydratedState
.has('text')) {
162 state
= state
.set('text', hydratedState
.get('text'));
168 const domParser
= new DOMParser();
170 const expandMentions
= status
=> {
171 const fragment
= domParser
.parseFromString(status
.get('content'), 'text/html').documentElement
;
173 status
.get('mentions').forEach(mention
=> {
174 fragment
.querySelector(`a[href="${mention.get('url')}"]`).textContent
= `@${mention.get('acct')}`;
177 return fragment
.innerHTML
;
180 export default function compose(state
= initialState
, action
) {
181 switch(action
.type
) {
183 return hydrate(state
, action
.state
.get('compose'));
185 return state
.set('mounted', state
.get('mounted') + 1);
186 case COMPOSE_UNMOUNT:
188 .set('mounted', Math
.max(state
.get('mounted') - 1, 0))
189 .set('is_composing', false);
190 case COMPOSE_SENSITIVITY_CHANGE:
191 return state
.withMutations(map
=> {
192 if (!state
.get('spoiler')) {
193 map
.set('sensitive', !state
.get('sensitive'));
196 map
.set('idempotencyKey', uuid());
198 case COMPOSE_SPOILERNESS_CHANGE:
199 return state
.withMutations(map
=> {
200 map
.set('spoiler_text', '');
201 map
.set('spoiler', !state
.get('spoiler'));
202 map
.set('idempotencyKey', uuid());
204 if (!state
.get('sensitive') && state
.get('media_attachments').size
>= 1) {
205 map
.set('sensitive', true);
208 case COMPOSE_SPOILER_TEXT_CHANGE:
210 .set('spoiler_text', action
.text
)
211 .set('idempotencyKey', uuid());
212 case COMPOSE_VISIBILITY_CHANGE:
214 .set('privacy', action
.value
)
215 .set('idempotencyKey', uuid());
218 .set('text', action
.text
)
219 .set('idempotencyKey', uuid());
220 case COMPOSE_COMPOSING_CHANGE:
221 return state
.set('is_composing', action
.value
);
223 return state
.withMutations(map
=> {
224 map
.set('in_reply_to', action
.status
.get('id'));
225 map
.set('text', statusToTextMentions(state
, action
.status
));
226 map
.set('privacy', privacyPreference(action
.status
.get('visibility'), state
.get('default_privacy')));
227 map
.set('focusDate', new Date());
228 map
.set('caretPosition', null);
229 map
.set('preselectDate', new Date());
230 map
.set('idempotencyKey', uuid());
232 if (action
.status
.get('spoiler_text').length
> 0) {
233 map
.set('spoiler', true);
234 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
236 map
.set('spoiler', false);
237 map
.set('spoiler_text', '');
240 case COMPOSE_REPLY_CANCEL:
242 return state
.withMutations(map
=> {
243 map
.set('in_reply_to', null);
245 map
.set('spoiler', false);
246 map
.set('spoiler_text', '');
247 map
.set('privacy', state
.get('default_privacy'));
248 map
.set('idempotencyKey', uuid());
250 case COMPOSE_SUBMIT_REQUEST:
251 case COMPOSE_UPLOAD_CHANGE_REQUEST:
252 return state
.set('is_submitting', true);
253 case COMPOSE_SUBMIT_SUCCESS:
254 return clearAll(state
);
255 case COMPOSE_SUBMIT_FAIL:
256 case COMPOSE_UPLOAD_CHANGE_FAIL:
257 return state
.set('is_submitting', false);
258 case COMPOSE_UPLOAD_REQUEST:
259 return state
.set('is_uploading', true);
260 case COMPOSE_UPLOAD_SUCCESS:
261 return appendMedia(state
, fromJS(action
.media
));
262 case COMPOSE_UPLOAD_FAIL:
263 return state
.set('is_uploading', false);
264 case COMPOSE_UPLOAD_UNDO:
265 return removeMedia(state
, action
.media_id
);
266 case COMPOSE_UPLOAD_PROGRESS:
267 return state
.set('progress', Math
.round((action
.loaded
/ action
.total
) * 100));
268 case COMPOSE_MENTION:
269 return state
.withMutations(map
=> {
270 map
.update('text', text
=> [text
.trim(), `@${action.account.get('acct')} `].filter((str
) => str
.length
!== 0).join(' '));
271 map
.set('focusDate', new Date());
272 map
.set('caretPosition', null);
273 map
.set('idempotencyKey', uuid());
276 return state
.withMutations(map
=> {
277 map
.update('text', text
=> [text
.trim(), `@${action.account.get('acct')} `].filter((str
) => str
.length
!== 0).join(' '));
278 map
.set('privacy', 'direct');
279 map
.set('focusDate', new Date());
280 map
.set('caretPosition', null);
281 map
.set('idempotencyKey', uuid());
283 case COMPOSE_SUGGESTIONS_CLEAR:
284 return state
.update('suggestions', ImmutableList(), list
=> list
.clear()).set('suggestion_token', null);
285 case COMPOSE_SUGGESTIONS_READY:
286 return state
.set('suggestions', ImmutableList(action
.accounts
? action
.accounts
.map(item
=> item
.id
) : action
.emojis
)).set('suggestion_token', action
.token
);
287 case COMPOSE_SUGGESTION_SELECT:
288 return insertSuggestion(state
, action
.position
, action
.token
, action
.completion
);
289 case COMPOSE_SUGGESTION_TAGS_UPDATE:
290 return updateSuggestionTags(state
, action
.token
);
291 case COMPOSE_TAG_HISTORY_UPDATE:
292 return state
.set('tagHistory', fromJS(action
.tags
));
293 case TIMELINE_DELETE:
294 if (action
.id
=== state
.get('in_reply_to')) {
295 return state
.set('in_reply_to', null);
299 case COMPOSE_EMOJI_INSERT:
300 return insertEmoji(state
, action
.position
, action
.emoji
, action
.needsSpace
);
301 case COMPOSE_UPLOAD_CHANGE_SUCCESS:
303 .set('is_submitting', false)
304 .update('media_attachments', list
=> list
.map(item
=> {
305 if (item
.get('id') === action
.media
.id
) {
306 return fromJS(action
.media
);
312 return state
.withMutations(map
=> {
313 map
.set('text', unescapeHTML(expandMentions(action
.status
)));
314 map
.set('in_reply_to', action
.status
.get('in_reply_to_id'));
315 map
.set('privacy', action
.status
.get('visibility'));
316 map
.set('media_attachments', action
.status
.get('media_attachments'));
317 map
.set('focusDate', new Date());
318 map
.set('caretPosition', null);
319 map
.set('idempotencyKey', uuid());
321 if (action
.status
.get('spoiler_text').length
> 0) {
322 map
.set('spoiler', true);
323 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
325 map
.set('spoiler', false);
326 map
.set('spoiler_text', '');