8 COMPOSE_SUBMIT_REQUEST
,
9 COMPOSE_SUBMIT_SUCCESS
,
11 COMPOSE_UPLOAD_REQUEST
,
12 COMPOSE_UPLOAD_SUCCESS
,
15 COMPOSE_UPLOAD_PROGRESS
,
16 COMPOSE_SUGGESTIONS_CLEAR
,
17 COMPOSE_SUGGESTIONS_READY
,
18 COMPOSE_SUGGESTION_SELECT
,
19 COMPOSE_SENSITIVITY_CHANGE
,
20 COMPOSE_SPOILERNESS_CHANGE
,
21 COMPOSE_SPOILER_TEXT_CHANGE
,
22 COMPOSE_VISIBILITY_CHANGE
,
23 COMPOSE_COMPOSING_CHANGE
,
25 COMPOSE_UPLOAD_CHANGE_REQUEST
,
26 COMPOSE_UPLOAD_CHANGE_SUCCESS
,
27 COMPOSE_UPLOAD_CHANGE_FAIL
,
29 } from '../actions/compose';
30 import { TIMELINE_DELETE
} from '../actions/timelines';
31 import { STORE_HYDRATE
} from '../actions/store';
32 import { Map as ImmutableMap
, List as ImmutableList
, OrderedSet as ImmutableOrderedSet
, fromJS
} from 'immutable';
33 import uuid
from '../uuid';
34 import { me
} from '../initial_state';
36 const initialState
= ImmutableMap({
50 media_attachments: ImmutableList(),
51 suggestion_token: null,
52 suggestions: ImmutableList(),
53 default_privacy: 'public',
54 default_sensitive: false,
55 resetFileKey: Math
.floor((Math
.random() * 0x10000)),
59 function statusToTextMentions(state
, status
) {
60 let set = ImmutableOrderedSet([]);
62 if (status
.getIn(['account', 'id']) !== me
) {
63 set = set.add(`@${status.getIn(['account', 'acct'])} `);
66 return set.union(status
.get('mentions').filterNot(mention
=> mention
.get('id') === me
).map(mention
=> `@${mention.get('acct')} `)).join('');
69 function clearAll(state
) {
70 return state
.withMutations(map
=> {
72 map
.set('spoiler', false);
73 map
.set('spoiler_text', '');
74 map
.set('is_submitting', false);
75 map
.set('in_reply_to', null);
76 map
.set('privacy', state
.get('default_privacy'));
77 map
.set('sensitive', false);
78 map
.update('media_attachments', list
=> list
.clear());
79 map
.set('idempotencyKey', uuid());
83 function appendMedia(state
, media
) {
84 const prevSize
= state
.get('media_attachments').size
;
86 return state
.withMutations(map
=> {
87 map
.update('media_attachments', list
=> list
.push(media
));
88 map
.set('is_uploading', false);
89 map
.set('resetFileKey', Math
.floor((Math
.random() * 0x10000)));
90 map
.update('text', oldText
=> `${oldText.trim()} ${media.get('text_url')}`);
91 map
.set('focusDate', new Date());
92 map
.set('idempotencyKey', uuid());
94 if (prevSize
=== 0 && (state
.get('default_sensitive') || state
.get('spoiler'))) {
95 map
.set('sensitive', true);
100 function removeMedia(state
, mediaId
) {
101 const media
= state
.get('media_attachments').find(item
=> item
.get('id') === mediaId
);
102 const prevSize
= state
.get('media_attachments').size
;
104 return state
.withMutations(map
=> {
105 map
.update('media_attachments', list
=> list
.filterNot(item
=> item
.get('id') === mediaId
));
106 map
.update('text', text
=> text
.replace(media
.get('text_url'), '').trim());
107 map
.set('idempotencyKey', uuid());
109 if (prevSize
=== 1) {
110 map
.set('sensitive', false);
115 const insertSuggestion
= (state
, position
, token
, completion
) => {
116 return state
.withMutations(map
=> {
117 map
.update('text', oldText
=> `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
118 map
.set('suggestion_token', null);
119 map
.update('suggestions', ImmutableList(), list
=> list
.clear());
120 map
.set('focusDate', new Date());
121 map
.set('idempotencyKey', uuid());
125 const insertEmoji
= (state
, position
, emojiData
) => {
126 const emoji
= emojiData
.native;
128 return state
.withMutations(map
=> {
129 map
.update('text', oldText
=> `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
130 map
.set('focusDate', new Date());
131 map
.set('idempotencyKey', uuid());
135 const privacyPreference
= (a
, b
) => {
136 if (a
=== 'direct' || b
=== 'direct') {
138 } else if (a
=== 'private' || b
=== 'private') {
140 } else if (a
=== 'unlisted' || b
=== 'unlisted') {
147 const hydrate
= (state
, hydratedState
) => {
148 state
= clearAll(state
.merge(hydratedState
));
150 if (hydratedState
.has('text')) {
151 state
= state
.set('text', hydratedState
.get('text'));
157 export default function compose(state
= initialState
, action
) {
158 switch(action
.type
) {
160 return hydrate(state
, action
.state
.get('compose'));
162 return state
.set('mounted', state
.get('mounted') + 1);
163 case COMPOSE_UNMOUNT:
165 .set('mounted', Math
.max(state
.get('mounted') - 1, 0))
166 .set('is_composing', false);
167 case COMPOSE_SENSITIVITY_CHANGE:
168 return state
.withMutations(map
=> {
169 if (!state
.get('spoiler')) {
170 map
.set('sensitive', !state
.get('sensitive'));
173 map
.set('idempotencyKey', uuid());
175 case COMPOSE_SPOILERNESS_CHANGE:
176 return state
.withMutations(map
=> {
177 map
.set('spoiler_text', '');
178 map
.set('spoiler', !state
.get('spoiler'));
179 map
.set('idempotencyKey', uuid());
181 if (!state
.get('sensitive') && state
.get('media_attachments').size
>= 1) {
182 map
.set('sensitive', true);
185 case COMPOSE_SPOILER_TEXT_CHANGE:
187 .set('spoiler_text', action
.text
)
188 .set('idempotencyKey', uuid());
189 case COMPOSE_VISIBILITY_CHANGE:
191 .set('privacy', action
.value
)
192 .set('idempotencyKey', uuid());
195 .set('text', action
.text
)
196 .set('idempotencyKey', uuid());
197 case COMPOSE_COMPOSING_CHANGE:
198 return state
.set('is_composing', action
.value
);
200 return state
.withMutations(map
=> {
201 map
.set('in_reply_to', action
.status
.get('id'));
202 map
.set('text', statusToTextMentions(state
, action
.status
));
203 map
.set('privacy', privacyPreference(action
.status
.get('visibility'), state
.get('default_privacy')));
204 map
.set('focusDate', new Date());
205 map
.set('preselectDate', new Date());
206 map
.set('idempotencyKey', uuid());
208 if (action
.status
.get('spoiler_text').length
> 0) {
209 map
.set('spoiler', true);
210 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
212 map
.set('spoiler', false);
213 map
.set('spoiler_text', '');
216 case COMPOSE_REPLY_CANCEL:
218 return state
.withMutations(map
=> {
219 map
.set('in_reply_to', null);
221 map
.set('spoiler', false);
222 map
.set('spoiler_text', '');
223 map
.set('privacy', state
.get('default_privacy'));
224 map
.set('idempotencyKey', uuid());
226 case COMPOSE_SUBMIT_REQUEST:
227 case COMPOSE_UPLOAD_CHANGE_REQUEST:
228 return state
.set('is_submitting', true);
229 case COMPOSE_SUBMIT_SUCCESS:
230 return clearAll(state
);
231 case COMPOSE_SUBMIT_FAIL:
232 case COMPOSE_UPLOAD_CHANGE_FAIL:
233 return state
.set('is_submitting', false);
234 case COMPOSE_UPLOAD_REQUEST:
235 return state
.set('is_uploading', true);
236 case COMPOSE_UPLOAD_SUCCESS:
237 return appendMedia(state
, fromJS(action
.media
));
238 case COMPOSE_UPLOAD_FAIL:
239 return state
.set('is_uploading', false);
240 case COMPOSE_UPLOAD_UNDO:
241 return removeMedia(state
, action
.media_id
);
242 case COMPOSE_UPLOAD_PROGRESS:
243 return state
.set('progress', Math
.round((action
.loaded
/ action
.total
) * 100));
244 case COMPOSE_MENTION:
246 .update('text', text
=> `${text}@${action.account.get('acct')} `)
247 .set('focusDate', new Date())
248 .set('idempotencyKey', uuid());
249 case COMPOSE_SUGGESTIONS_CLEAR:
250 return state
.update('suggestions', ImmutableList(), list
=> list
.clear()).set('suggestion_token', null);
251 case COMPOSE_SUGGESTIONS_READY:
252 return state
.set('suggestions', ImmutableList(action
.accounts
? action
.accounts
.map(item
=> item
.id
) : action
.emojis
)).set('suggestion_token', action
.token
);
253 case COMPOSE_SUGGESTION_SELECT:
254 return insertSuggestion(state
, action
.position
, action
.token
, action
.completion
);
255 case TIMELINE_DELETE:
256 if (action
.id
=== state
.get('in_reply_to')) {
257 return state
.set('in_reply_to', null);
261 case COMPOSE_EMOJI_INSERT:
262 return insertEmoji(state
, action
.position
, action
.emoji
);
263 case COMPOSE_UPLOAD_CHANGE_SUCCESS:
265 .set('is_submitting', false)
266 .update('media_attachments', list
=> list
.map(item
=> {
267 if (item
.get('id') === action
.media
.id
) {
268 return item
.set('description', action
.media
.description
);