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 } from '../actions/compose';
26 import { TIMELINE_DELETE
} from '../actions/timelines';
27 import { STORE_HYDRATE
} from '../actions/store';
28 import { Map as ImmutableMap
, List as ImmutableList
, OrderedSet as ImmutableOrderedSet
, fromJS
} from 'immutable';
29 import uuid
from '../uuid';
31 const initialState
= ImmutableMap({
45 media_attachments: ImmutableList(),
46 suggestion_token: null,
47 suggestions: ImmutableList(),
49 default_privacy: 'public',
50 default_sensitive: false,
51 resetFileKey: Math
.floor((Math
.random() * 0x10000)),
55 function statusToTextMentions(state
, status
) {
56 let set = ImmutableOrderedSet([]);
57 let me
= state
.get('me');
59 if (status
.getIn(['account', 'id']) !== me
) {
60 set = set.add(`@${status.getIn(['account', 'acct'])} `);
63 return set.union(status
.get('mentions').filterNot(mention
=> mention
.get('id') === me
).map(mention
=> `@${mention.get('acct')} `)).join('');
66 function clearAll(state
) {
67 return state
.withMutations(map
=> {
69 map
.set('spoiler', false);
70 map
.set('spoiler_text', '');
71 map
.set('is_submitting', false);
72 map
.set('in_reply_to', null);
73 map
.set('privacy', state
.get('default_privacy'));
74 map
.set('sensitive', false);
75 map
.update('media_attachments', list
=> list
.clear());
76 map
.set('idempotencyKey', uuid());
80 function appendMedia(state
, media
) {
81 const prevSize
= state
.get('media_attachments').size
;
83 return state
.withMutations(map
=> {
84 map
.update('media_attachments', list
=> list
.push(media
));
85 map
.set('is_uploading', false);
86 map
.set('resetFileKey', Math
.floor((Math
.random() * 0x10000)));
87 map
.update('text', oldText
=> `${oldText.trim()} ${media.get('text_url')}`);
88 map
.set('focusDate', new Date());
89 map
.set('idempotencyKey', uuid());
91 if (prevSize
=== 0 && (state
.get('default_sensitive') || state
.get('spoiler'))) {
92 map
.set('sensitive', true);
97 function removeMedia(state
, mediaId
) {
98 const media
= state
.get('media_attachments').find(item
=> item
.get('id') === mediaId
);
99 const prevSize
= state
.get('media_attachments').size
;
101 return state
.withMutations(map
=> {
102 map
.update('media_attachments', list
=> list
.filterNot(item
=> item
.get('id') === mediaId
));
103 map
.update('text', text
=> text
.replace(media
.get('text_url'), '').trim());
104 map
.set('idempotencyKey', uuid());
106 if (prevSize
=== 1) {
107 map
.set('sensitive', false);
112 const insertSuggestion
= (state
, position
, token
, completion
) => {
113 return state
.withMutations(map
=> {
114 map
.update('text', oldText
=> `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
115 map
.set('suggestion_token', null);
116 map
.update('suggestions', ImmutableList(), list
=> list
.clear());
117 map
.set('focusDate', new Date());
118 map
.set('idempotencyKey', uuid());
122 const insertEmoji
= (state
, position
, emojiData
) => {
123 const emoji
= emojiData
.unicode
.split('-').map(code
=> String
.fromCodePoint(parseInt(code
, 16))).join('');
125 return state
.withMutations(map
=> {
126 map
.update('text', oldText
=> `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
127 map
.set('focusDate', new Date());
128 map
.set('idempotencyKey', uuid());
132 const privacyPreference
= (a
, b
) => {
133 if (a
=== 'direct' || b
=== 'direct') {
135 } else if (a
=== 'private' || b
=== 'private') {
137 } else if (a
=== 'unlisted' || b
=== 'unlisted') {
144 export default function compose(state
= initialState
, action
) {
145 switch(action
.type
) {
147 return clearAll(state
.merge(action
.state
.get('compose')));
149 return state
.set('mounted', true);
150 case COMPOSE_UNMOUNT:
152 .set('mounted', false)
153 .set('is_composing', false);
154 case COMPOSE_SENSITIVITY_CHANGE:
155 return state
.withMutations(map
=> {
156 if (!state
.get('spoiler')) {
157 map
.set('sensitive', !state
.get('sensitive'));
160 map
.set('idempotencyKey', uuid());
162 case COMPOSE_SPOILERNESS_CHANGE:
163 return state
.withMutations(map
=> {
164 map
.set('spoiler_text', '');
165 map
.set('spoiler', !state
.get('spoiler'));
166 map
.set('idempotencyKey', uuid());
168 if (!state
.get('sensitive') && state
.get('media_attachments').size
>= 1) {
169 map
.set('sensitive', true);
172 case COMPOSE_SPOILER_TEXT_CHANGE:
174 .set('spoiler_text', action
.text
)
175 .set('idempotencyKey', uuid());
176 case COMPOSE_VISIBILITY_CHANGE:
178 .set('privacy', action
.value
)
179 .set('idempotencyKey', uuid());
182 .set('text', action
.text
)
183 .set('idempotencyKey', uuid());
184 case COMPOSE_COMPOSING_CHANGE:
185 return state
.set('is_composing', action
.value
);
187 return state
.withMutations(map
=> {
188 map
.set('in_reply_to', action
.status
.get('id'));
189 map
.set('text', statusToTextMentions(state
, action
.status
));
190 map
.set('privacy', privacyPreference(action
.status
.get('visibility'), state
.get('default_privacy')));
191 map
.set('focusDate', new Date());
192 map
.set('preselectDate', new Date());
193 map
.set('idempotencyKey', uuid());
195 if (action
.status
.get('spoiler_text').length
> 0) {
196 map
.set('spoiler', true);
197 map
.set('spoiler_text', action
.status
.get('spoiler_text'));
199 map
.set('spoiler', false);
200 map
.set('spoiler_text', '');
203 case COMPOSE_REPLY_CANCEL:
204 return state
.withMutations(map
=> {
205 map
.set('in_reply_to', null);
207 map
.set('spoiler', false);
208 map
.set('spoiler_text', '');
209 map
.set('privacy', state
.get('default_privacy'));
210 map
.set('idempotencyKey', uuid());
212 case COMPOSE_SUBMIT_REQUEST:
213 return state
.set('is_submitting', true);
214 case COMPOSE_SUBMIT_SUCCESS:
215 return clearAll(state
);
216 case COMPOSE_SUBMIT_FAIL:
217 return state
.set('is_submitting', false);
218 case COMPOSE_UPLOAD_REQUEST:
219 return state
.withMutations(map
=> {
220 map
.set('is_uploading', true);
222 case COMPOSE_UPLOAD_SUCCESS:
223 return appendMedia(state
, fromJS(action
.media
));
224 case COMPOSE_UPLOAD_FAIL:
225 return state
.set('is_uploading', false);
226 case COMPOSE_UPLOAD_UNDO:
227 return removeMedia(state
, action
.media_id
);
228 case COMPOSE_UPLOAD_PROGRESS:
229 return state
.set('progress', Math
.round((action
.loaded
/ action
.total
) * 100));
230 case COMPOSE_MENTION:
232 .update('text', text
=> `${text}@${action.account.get('acct')} `)
233 .set('focusDate', new Date())
234 .set('idempotencyKey', uuid());
235 case COMPOSE_SUGGESTIONS_CLEAR:
236 return state
.update('suggestions', ImmutableList(), list
=> list
.clear()).set('suggestion_token', null);
237 case COMPOSE_SUGGESTIONS_READY:
238 return state
.set('suggestions', ImmutableList(action
.accounts
.map(item
=> item
.id
))).set('suggestion_token', action
.token
);
239 case COMPOSE_SUGGESTION_SELECT:
240 return insertSuggestion(state
, action
.position
, action
.token
, action
.completion
);
241 case TIMELINE_DELETE:
242 if (action
.id
=== state
.get('in_reply_to')) {
243 return state
.set('in_reply_to', null);
247 case COMPOSE_EMOJI_INSERT:
248 return insertEmoji(state
, action
.position
, action
.emoji
);