status: status,
});
- if (!getState().getIn(['compose', 'mounted'])) {
+ if (router && !getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
};
}).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data }));
+ // If the response has no data then we can't do anything else.
+ if (!response.data) {
+ return;
+ }
+
// To make the app more responsive, immediately get the status into the columns
const insertOrRefresh = (timelineId, refreshAction) => {
};
};
-export function toggleComposeAdvancedOption(option) {
+export function changeComposeAdvancedOption(option, value) {
return {
+ option,
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
- option: option,
+ value,
};
}
import {
cancelReplyCompose,
changeCompose,
+ changeComposeAdvancedOption,
changeComposeSensitivity,
changeComposeSpoilerText,
changeComposeSpoilerness,
mountCompose,
selectComposeSuggestion,
submitCompose,
- toggleComposeAdvancedOption,
undoUploadCompose,
unmountCompose,
uploadCompose,
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
return {
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
+ advancedOptions: state.getIn(['compose', 'advanced_options']),
amUnlocked: !state.getIn(['accounts', me, 'locked']),
- doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
focusDate: state.getIn(['compose', 'focusDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isUploading: state.getIn(['compose', 'is_uploading']),
// Dispatch mapping.
const mapDispatchToProps = {
onCancelReply: cancelReplyCompose,
+ onChangeAdvancedOption: changeComposeAdvancedOption,
onChangeDescription: changeUploadCompose,
onChangeSensitivity: changeComposeSensitivity,
onChangeSpoilerText: changeComposeSpoilerText,
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
onSelectSuggestion: selectComposeSuggestion,
onSubmit: submitCompose,
- onToggleAdvancedOption: toggleComposeAdvancedOption,
onUndoUpload: undoUploadCompose,
onUnmount: unmountCompose,
onUpload: uploadCompose,
} = this.handlers;
const {
acceptContentTypes,
+ advancedOptions,
amUnlocked,
- doNotFederate,
intl,
isSubmitting,
isUploading,
layout,
media,
onCancelReply,
+ onChangeAdvancedOption,
onChangeDescription,
onChangeSensitivity,
onChangeSpoilerness,
onFetchSuggestions,
onOpenActionsModal,
onOpenDoodleModal,
- onToggleAdvancedOption,
onUndoUpload,
onUpload,
privacy,
/>
) : null}
<ComposerTextarea
+ advancedOptions={advancedOptions}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
disabled={isSubmitting}
intl={intl}
) : null}
<ComposerOptions
acceptContentTypes={acceptContentTypes}
+ advancedOptions={advancedOptions}
disabled={isSubmitting}
- doNotFederate={doNotFederate}
full={media.size >= 4 || media.some(
item => item.get('type') === 'video'
)}
hasMedia={!!media.size}
intl={intl}
+ onChangeAdvancedOption={onChangeAdvancedOption}
onChangeSensitivity={onChangeSensitivity}
onChangeVisibility={onChangeVisibility}
onDoodleOpen={onOpenDoodleModal}
onModalClose={onCloseModal}
onModalOpen={onOpenActionsModal}
- onToggleAdvancedOption={onToggleAdvancedOption}
onToggleSpoiler={onChangeSpoilerness}
onUpload={onUpload}
privacy={privacy}
spoiler={spoiler}
/>
<ComposerPublisher
- countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
+ countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
intl={intl}
onSecondarySubmit={handleSecondarySubmit}
// State props.
acceptContentTypes: PropTypes.string,
+ advancedOptions: ImmutablePropTypes.map,
amUnlocked: PropTypes.bool,
- doNotFederate: PropTypes.bool,
focusDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isUploading: PropTypes.bool,
// Dispatch props.
onCancelReply: PropTypes.func,
+ onChangeAdvancedOption: PropTypes.func,
onChangeDescription: PropTypes.func,
onChangeSensitivity: PropTypes.func,
onChangeSpoilerText: PropTypes.func,
onOpenDoodleModal: PropTypes.func,
onSelectSuggestion: PropTypes.func,
onSubmit: PropTypes.func,
- onToggleAdvancedOption: PropTypes.func,
onUndoUpload: PropTypes.func,
onUnmount: PropTypes.func,
onUpload: PropTypes.func,
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
import {
FormattedMessage,
defineMessages,
},
local_only_long: {
defaultMessage: 'Do not post to other instances',
- id: 'advanced-options.local-only.long',
+ id: 'advanced_options.local-only.long',
},
local_only_short: {
defaultMessage: 'Local-only',
- id: 'advanced-options.local-only.short',
+ id: 'advanced_options.local-only.short',
},
private_long: {
defaultMessage: 'Post to followers only',
defaultMessage: 'Hide text behind warning',
id: 'compose_form.spoiler',
},
+ threaded_mode_long: {
+ defaultMessage: 'Automatically opens a reply on posting',
+ id: 'advanced_options.threaded_mode.long',
+ },
+ threaded_mode_short: {
+ defaultMessage: 'Threaded mode',
+ id: 'advanced_options.threaded_mode.short',
+ },
unlisted_long: {
defaultMessage: 'Do not show in public timelines',
id: 'privacy.unlisted.long',
} = this.handlers;
const {
acceptContentTypes,
+ advancedOptions,
disabled,
- doNotFederate,
full,
hasMedia,
intl,
+ onChangeAdvancedOption,
onChangeSensitivity,
onChangeVisibility,
onModalClose,
onModalOpen,
- onToggleAdvancedOption,
onToggleSpoiler,
privacy,
resetFileKey,
onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)}
/>
- <Dropdown
- active={doNotFederate}
- disabled={disabled}
- icon='home'
- items={[
- {
- meta: <FormattedMessage {...messages.local_only_long} />,
- name: 'do_not_federate',
- on: doNotFederate,
- text: <FormattedMessage {...messages.local_only_short} />,
- },
- ]}
- onChange={onToggleAdvancedOption}
- onModalClose={onModalClose}
- onModalOpen={onModalOpen}
- title={intl.formatMessage(messages.advanced_options_icon_title)}
- />
+ {advancedOptions ? (
+ <Dropdown
+ active={advancedOptions.some(value => !!value)}
+ disabled={disabled}
+ icon='ellipsis-h'
+ items={[
+ {
+ meta: <FormattedMessage {...messages.local_only_long} />,
+ name: 'do_not_federate',
+ on: advancedOptions.get('do_not_federate'),
+ text: <FormattedMessage {...messages.local_only_short} />,
+ },
+ {
+ meta: <FormattedMessage {...messages.threaded_mode_long} />,
+ name: 'threaded_mode',
+ on: advancedOptions.get('threaded_mode'),
+ text: <FormattedMessage {...messages.threaded_mode_short} />,
+ },
+ ]}
+ onChange={onChangeAdvancedOption}
+ onModalClose={onModalClose}
+ onModalOpen={onModalOpen}
+ title={intl.formatMessage(messages.advanced_options_icon_title)}
+ />
+ ) : null}
</div>
);
}
// Props.
ComposerOptions.propTypes = {
acceptContentTypes: PropTypes.string,
+ advancedOptions: ImmutablePropTypes.map,
disabled: PropTypes.bool,
- doNotFederate: PropTypes.bool,
full: PropTypes.bool,
hasMedia: PropTypes.bool,
intl: PropTypes.object.isRequired,
+ onChangeAdvancedOption: PropTypes.func,
onChangeSensitivity: PropTypes.func,
onChangeVisibility: PropTypes.func,
onDoodleOpen: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
- onToggleAdvancedOption: PropTypes.func,
onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func,
privacy: PropTypes.string,
// Components.
import EmojiPicker from 'flavours/glitch/features/emoji_picker';
+import ComposerTextareaIcons from './icons';
import ComposerTextareaSuggestions from './suggestions';
// Utils.
handleRefTextarea,
} = this.handlers;
const {
+ advancedOptions,
autoFocus,
disabled,
intl,
<div className='composer--textarea'>
<label>
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
+ <ComposerTextareaIcons
+ advancedOptions={advancedOptions}
+ intl={intl}
+ />
<Textarea
aria-autocomplete='list'
autoFocus={autoFocus}
// Props.
ComposerTextarea.propTypes = {
+ advancedOptions: ImmutablePropTypes.map,
autoFocus: PropTypes.bool,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
'compose.attach.doodle': 'Draw something',
'compose.attach': 'Attach...',
- 'advanced-options.local-only.short': 'Local-only',
- 'advanced-options.local-only.long': 'Do not post to other instances',
+ 'advanced_options.local-only.short': 'Local-only',
+ 'advanced_options.local-only.long': 'Do not post to other instances',
+ 'advanced_options.local-only.tooltip': 'This post is local-only',
'advanced_options.icon_title': 'Advanced options',
+ 'advanced_options.threaded_mode.short': 'Threaded mode',
+ 'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
+ 'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
};
export default Object.assign({}, inherited, messages);
'compose.attach.doodle': '落書きをする',
'compose.attach': 'アタッチ...',
- 'advanced-options.local-only.short': 'ローカル限定',
- 'advanced-options.local-only.long': '他のインスタンスには投稿されません',
+ 'advanced_options.local-only.short': 'ローカル限定',
+ 'advanced_options.local-only.long': '他のインスタンスには投稿されません',
'advanced_options.icon_title': '高度な設定',
};
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from 'flavours/glitch/util/uuid';
import { me } from 'flavours/glitch/util/initial_state';
+import { overwrite } from 'flavours/glitch/util/js_helpers';
const initialState = ImmutableMap({
mounted: false,
advanced_options: ImmutableMap({
do_not_federate: false,
+ threaded_mode: false,
}),
sensitive: false,
spoiler: false,
suggestions: ImmutableList(),
default_advanced_options: ImmutableMap({
do_not_federate: false,
+ threaded_mode: null, // Do not reset
}),
default_privacy: 'public',
default_sensitive: false,
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
};
+function apiStatusToTextMentions (state, status) {
+ let set = ImmutableOrderedSet([]);
+
+ if (status.account.id !== me) {
+ set = set.add(`@${status.account.acct} `);
+ }
+
+ return set.union(status.mentions.filter(
+ mention => mention.id !== me
+ ).map(
+ mention => `@${mention.acct} `
+ )).join('');
+}
+
function clearAll(state) {
return state.withMutations(map => {
map.set('text', '');
map.set('spoiler_text', '');
map.set('is_submitting', false);
map.set('in_reply_to', null);
- map.set('advanced_options', state.get('default_advanced_options'));
+ map.update(
+ 'advanced_options',
+ map => map.mergeWith(overwrite, state.get('default_advanced_options'))
+ );
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false);
map.update('media_attachments', list => list.clear());
});
};
+function continueThread (state, status) {
+ return state.withMutations(function (map) {
+ map.set('text', apiStatusToTextMentions(state, status));
+ if (status.spoiler_text) {
+ map.set('spoiler', true);
+ map.set('spoiler_text', status.spoiler_text);
+ } else {
+ map.set('spoiler', false);
+ map.set('spoiler_text', '');
+ }
+ map.set('is_submitting', false);
+ map.set('in_reply_to', status.id);
+ map.update(
+ 'advanced_options',
+ map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
+ );
+ map.set('privacy', privacyPreference(status.visibility, state.get('default_privacy')));
+ map.set('sensitive', false);
+ map.update('media_attachments', list => list.clear());
+ map.set('idempotencyKey', uuid());
+ map.set('focusDate', new Date());
+ map.set('preselectDate', new Date());
+ });
+}
+
function appendMedia(state, media) {
const prevSize = state.get('media_attachments').size;
return state.set('mounted', false);
case COMPOSE_ADVANCED_OPTIONS_CHANGE:
return state
- .set('advanced_options',
- state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
+ .set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
.set('idempotencyKey', uuid());
case COMPOSE_SENSITIVITY_CHANGE:
return state.withMutations(map => {
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
- map.set('advanced_options', new ImmutableMap({
- do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
- }));
+ map.update(
+ 'advanced_options',
+ map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
+ );
map.set('focusDate', new Date());
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('privacy', state.get('default_privacy'));
- map.set('advanced_options', state.get('default_advanced_options'));
+ map.update(
+ 'advanced_options',
+ map => map.mergeWith(overwrite, state.get('default_advanced_options'))
+ );
map.set('idempotencyKey', uuid());
});
case COMPOSE_SUBMIT_REQUEST:
case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_submitting', true);
case COMPOSE_SUBMIT_SUCCESS:
- return clearAll(state);
+ return action.status && state.get('advanced_options', 'threaded_mode') ? continueThread(state, action.status) : clearAll(state);
case COMPOSE_SUBMIT_FAIL:
case COMPOSE_UPLOAD_CHANGE_FAIL:
return state.set('is_submitting', false);
}
}
+.composer--textarea--icons {
+ display: block;
+ position: absolute;
+ top: 29px;
+ right: 5px;
+ bottom: 5px;
+ overflow: hidden;
+
+ & > .textarea_icon {
+ display: block;
+ margin: 2px 0 0 2px;
+ width: 24px;
+ height: 24px;
+ color: darken($ui-primary-color, 24%);
+ font-size: 18px;
+ line-height: 24px;
+ text-align: center;
+ opacity: .8;
+ }
+}
+
.composer--textarea--suggestions {
display: block;
position: absolute;
--- /dev/null
+// This function returns the new value unless it is `null` or
+// `undefined`, in which case it returns the old one.
+export function overwrite (oldVal, newVal) {
+ return newVal === null || typeof newVal === 'undefined' ? oldVal : newVal;
+}