Threaded mode~

pull/296/head
kibigo! 2018-01-05 18:23:06 -08:00
parent 6932b464e6
commit e4bc013d6f
9 changed files with 157 additions and 48 deletions

View File

@ -61,7 +61,7 @@ export function replyCompose(status, router) {
status: status, status: status,
}); });
if (!getState().getIn(['compose', 'mounted'])) { if (router && !getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new'); router.push('/statuses/new');
} }
}; };
@ -118,6 +118,11 @@ export function submitCompose() {
}).then(function (response) { }).then(function (response) {
dispatch(submitComposeSuccess({ ...response.data })); 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 // To make the app more responsive, immediately get the status into the columns
const insertOrRefresh = (timelineId, refreshAction) => { const insertOrRefresh = (timelineId, refreshAction) => {
@ -341,10 +346,11 @@ export function unmountCompose() {
}; };
}; };
export function toggleComposeAdvancedOption(option) { export function changeComposeAdvancedOption(option, value) {
return { return {
option,
type: COMPOSE_ADVANCED_OPTIONS_CHANGE, type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
option: option, value,
}; };
} }

View File

@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { import {
cancelReplyCompose, cancelReplyCompose,
changeCompose, changeCompose,
changeComposeAdvancedOption,
changeComposeSensitivity, changeComposeSensitivity,
changeComposeSpoilerText, changeComposeSpoilerText,
changeComposeSpoilerness, changeComposeSpoilerness,
@ -18,7 +19,6 @@ import {
mountCompose, mountCompose,
selectComposeSuggestion, selectComposeSuggestion,
submitCompose, submitCompose,
toggleComposeAdvancedOption,
undoUploadCompose, undoUploadCompose,
unmountCompose, unmountCompose,
uploadCompose, uploadCompose,
@ -49,8 +49,8 @@ function mapStateToProps (state) {
const inReplyTo = state.getIn(['compose', 'in_reply_to']); const inReplyTo = state.getIn(['compose', 'in_reply_to']);
return { return {
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
advancedOptions: state.getIn(['compose', 'advanced_options']),
amUnlocked: !state.getIn(['accounts', me, 'locked']), amUnlocked: !state.getIn(['accounts', me, 'locked']),
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
focusDate: state.getIn(['compose', 'focusDate']), focusDate: state.getIn(['compose', 'focusDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']), isSubmitting: state.getIn(['compose', 'is_submitting']),
isUploading: state.getIn(['compose', 'is_uploading']), isUploading: state.getIn(['compose', 'is_uploading']),
@ -76,6 +76,7 @@ function mapStateToProps (state) {
// Dispatch mapping. // Dispatch mapping.
const mapDispatchToProps = { const mapDispatchToProps = {
onCancelReply: cancelReplyCompose, onCancelReply: cancelReplyCompose,
onChangeAdvancedOption: changeComposeAdvancedOption,
onChangeDescription: changeUploadCompose, onChangeDescription: changeUploadCompose,
onChangeSensitivity: changeComposeSensitivity, onChangeSensitivity: changeComposeSensitivity,
onChangeSpoilerText: changeComposeSpoilerText, onChangeSpoilerText: changeComposeSpoilerText,
@ -91,7 +92,6 @@ const mapDispatchToProps = {
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }), onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
onSelectSuggestion: selectComposeSuggestion, onSelectSuggestion: selectComposeSuggestion,
onSubmit: submitCompose, onSubmit: submitCompose,
onToggleAdvancedOption: toggleComposeAdvancedOption,
onUndoUpload: undoUploadCompose, onUndoUpload: undoUploadCompose,
onUnmount: unmountCompose, onUnmount: unmountCompose,
onUpload: uploadCompose, onUpload: uploadCompose,
@ -267,14 +267,15 @@ class Composer extends React.Component {
} = this.handlers; } = this.handlers;
const { const {
acceptContentTypes, acceptContentTypes,
advancedOptions,
amUnlocked, amUnlocked,
doNotFederate,
intl, intl,
isSubmitting, isSubmitting,
isUploading, isUploading,
layout, layout,
media, media,
onCancelReply, onCancelReply,
onChangeAdvancedOption,
onChangeDescription, onChangeDescription,
onChangeSensitivity, onChangeSensitivity,
onChangeSpoilerness, onChangeSpoilerness,
@ -285,7 +286,6 @@ class Composer extends React.Component {
onFetchSuggestions, onFetchSuggestions,
onOpenActionsModal, onOpenActionsModal,
onOpenDoodleModal, onOpenDoodleModal,
onToggleAdvancedOption,
onUndoUpload, onUndoUpload,
onUpload, onUpload,
privacy, privacy,
@ -321,6 +321,7 @@ class Composer extends React.Component {
/> />
) : null} ) : null}
<ComposerTextarea <ComposerTextarea
advancedOptions={advancedOptions}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
disabled={isSubmitting} disabled={isSubmitting}
intl={intl} intl={intl}
@ -347,19 +348,19 @@ class Composer extends React.Component {
) : null} ) : null}
<ComposerOptions <ComposerOptions
acceptContentTypes={acceptContentTypes} acceptContentTypes={acceptContentTypes}
advancedOptions={advancedOptions}
disabled={isSubmitting} disabled={isSubmitting}
doNotFederate={doNotFederate}
full={media.size >= 4 || media.some( full={media.size >= 4 || media.some(
item => item.get('type') === 'video' item => item.get('type') === 'video'
)} )}
hasMedia={!!media.size} hasMedia={!!media.size}
intl={intl} intl={intl}
onChangeAdvancedOption={onChangeAdvancedOption}
onChangeSensitivity={onChangeSensitivity} onChangeSensitivity={onChangeSensitivity}
onChangeVisibility={onChangeVisibility} onChangeVisibility={onChangeVisibility}
onDoodleOpen={onOpenDoodleModal} onDoodleOpen={onOpenDoodleModal}
onModalClose={onCloseModal} onModalClose={onCloseModal}
onModalOpen={onOpenActionsModal} onModalOpen={onOpenActionsModal}
onToggleAdvancedOption={onToggleAdvancedOption}
onToggleSpoiler={onChangeSpoilerness} onToggleSpoiler={onChangeSpoilerness}
onUpload={onUpload} onUpload={onUpload}
privacy={privacy} privacy={privacy}
@ -368,7 +369,7 @@ class Composer extends React.Component {
spoiler={spoiler} spoiler={spoiler}
/> />
<ComposerPublisher <ComposerPublisher
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`} countText={`${spoilerText}${countableText(text)}${advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length} disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
intl={intl} intl={intl}
onSecondarySubmit={handleSecondarySubmit} onSecondarySubmit={handleSecondarySubmit}
@ -388,8 +389,8 @@ Composer.propTypes = {
// State props. // State props.
acceptContentTypes: PropTypes.string, acceptContentTypes: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
amUnlocked: PropTypes.bool, amUnlocked: PropTypes.bool,
doNotFederate: PropTypes.bool,
focusDate: PropTypes.instanceOf(Date), focusDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool, isSubmitting: PropTypes.bool,
isUploading: PropTypes.bool, isUploading: PropTypes.bool,
@ -412,6 +413,7 @@ Composer.propTypes = {
// Dispatch props. // Dispatch props.
onCancelReply: PropTypes.func, onCancelReply: PropTypes.func,
onChangeAdvancedOption: PropTypes.func,
onChangeDescription: PropTypes.func, onChangeDescription: PropTypes.func,
onChangeSensitivity: PropTypes.func, onChangeSensitivity: PropTypes.func,
onChangeSpoilerText: PropTypes.func, onChangeSpoilerText: PropTypes.func,
@ -427,7 +429,6 @@ Composer.propTypes = {
onOpenDoodleModal: PropTypes.func, onOpenDoodleModal: PropTypes.func,
onSelectSuggestion: PropTypes.func, onSelectSuggestion: PropTypes.func,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onUndoUpload: PropTypes.func, onUndoUpload: PropTypes.func,
onUnmount: PropTypes.func, onUnmount: PropTypes.func,
onUpload: PropTypes.func, onUpload: PropTypes.func,

View File

@ -1,6 +1,7 @@
// Package imports. // Package imports.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { import {
FormattedMessage, FormattedMessage,
defineMessages, defineMessages,
@ -47,11 +48,11 @@ const messages = defineMessages({
}, },
local_only_long: { local_only_long: {
defaultMessage: 'Do not post to other instances', defaultMessage: 'Do not post to other instances',
id: 'advanced-options.local-only.long', id: 'advanced_options.local-only.long',
}, },
local_only_short: { local_only_short: {
defaultMessage: 'Local-only', defaultMessage: 'Local-only',
id: 'advanced-options.local-only.short', id: 'advanced_options.local-only.short',
}, },
private_long: { private_long: {
defaultMessage: 'Post to followers only', defaultMessage: 'Post to followers only',
@ -77,6 +78,14 @@ const messages = defineMessages({
defaultMessage: 'Hide text behind warning', defaultMessage: 'Hide text behind warning',
id: 'compose_form.spoiler', 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: { unlisted_long: {
defaultMessage: 'Do not show in public timelines', defaultMessage: 'Do not show in public timelines',
id: 'privacy.unlisted.long', id: 'privacy.unlisted.long',
@ -149,16 +158,16 @@ export default class ComposerOptions extends React.PureComponent {
} = this.handlers; } = this.handlers;
const { const {
acceptContentTypes, acceptContentTypes,
advancedOptions,
disabled, disabled,
doNotFederate,
full, full,
hasMedia, hasMedia,
intl, intl,
onChangeAdvancedOption,
onChangeSensitivity, onChangeSensitivity,
onChangeVisibility, onChangeVisibility,
onModalClose, onModalClose,
onModalOpen, onModalOpen,
onToggleAdvancedOption,
onToggleSpoiler, onToggleSpoiler,
privacy, privacy,
resetFileKey, resetFileKey,
@ -283,23 +292,31 @@ export default class ComposerOptions extends React.PureComponent {
onClick={onToggleSpoiler} onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)} title={intl.formatMessage(messages.spoiler)}
/> />
<Dropdown {advancedOptions ? (
active={doNotFederate} <Dropdown
disabled={disabled} active={advancedOptions.some(value => !!value)}
icon='home' disabled={disabled}
items={[ icon='ellipsis-h'
{ items={[
meta: <FormattedMessage {...messages.local_only_long} />, {
name: 'do_not_federate', meta: <FormattedMessage {...messages.local_only_long} />,
on: doNotFederate, name: 'do_not_federate',
text: <FormattedMessage {...messages.local_only_short} />, on: advancedOptions.get('do_not_federate'),
}, text: <FormattedMessage {...messages.local_only_short} />,
]} },
onChange={onToggleAdvancedOption} {
onModalClose={onModalClose} meta: <FormattedMessage {...messages.threaded_mode_long} />,
onModalOpen={onModalOpen} name: 'threaded_mode',
title={intl.formatMessage(messages.advanced_options_icon_title)} 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> </div>
); );
} }
@ -309,17 +326,17 @@ export default class ComposerOptions extends React.PureComponent {
// Props. // Props.
ComposerOptions.propTypes = { ComposerOptions.propTypes = {
acceptContentTypes: PropTypes.string, acceptContentTypes: PropTypes.string,
advancedOptions: ImmutablePropTypes.map,
disabled: PropTypes.bool, disabled: PropTypes.bool,
doNotFederate: PropTypes.bool,
full: PropTypes.bool, full: PropTypes.bool,
hasMedia: PropTypes.bool, hasMedia: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onChangeAdvancedOption: PropTypes.func,
onChangeSensitivity: PropTypes.func, onChangeSensitivity: PropTypes.func,
onChangeVisibility: PropTypes.func, onChangeVisibility: PropTypes.func,
onDoodleOpen: PropTypes.func, onDoodleOpen: PropTypes.func,
onModalClose: PropTypes.func, onModalClose: PropTypes.func,
onModalOpen: PropTypes.func, onModalOpen: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onToggleSpoiler: PropTypes.func, onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func, onUpload: PropTypes.func,
privacy: PropTypes.string, privacy: PropTypes.string,

View File

@ -10,6 +10,7 @@ import Textarea from 'react-textarea-autosize';
// Components. // Components.
import EmojiPicker from 'flavours/glitch/features/emoji_picker'; import EmojiPicker from 'flavours/glitch/features/emoji_picker';
import ComposerTextareaIcons from './icons';
import ComposerTextareaSuggestions from './suggestions'; import ComposerTextareaSuggestions from './suggestions';
// Utils. // Utils.
@ -232,6 +233,7 @@ export default class ComposerTextarea extends React.Component {
handleRefTextarea, handleRefTextarea,
} = this.handlers; } = this.handlers;
const { const {
advancedOptions,
autoFocus, autoFocus,
disabled, disabled,
intl, intl,
@ -249,6 +251,10 @@ export default class ComposerTextarea extends React.Component {
<div className='composer--textarea'> <div className='composer--textarea'>
<label> <label>
<span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span> <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span>
<ComposerTextareaIcons
advancedOptions={advancedOptions}
intl={intl}
/>
<Textarea <Textarea
aria-autocomplete='list' aria-autocomplete='list'
autoFocus={autoFocus} autoFocus={autoFocus}
@ -280,6 +286,7 @@ export default class ComposerTextarea extends React.Component {
// Props. // Props.
ComposerTextarea.propTypes = { ComposerTextarea.propTypes = {
advancedOptions: ImmutablePropTypes.map,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -52,9 +52,13 @@ const messages = {
'compose.attach.doodle': 'Draw something', 'compose.attach.doodle': 'Draw something',
'compose.attach': 'Attach...', 'compose.attach': 'Attach...',
'advanced-options.local-only.short': 'Local-only', 'advanced_options.local-only.short': 'Local-only',
'advanced-options.local-only.long': 'Do not post to other instances', '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.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); export default Object.assign({}, inherited, messages);

View File

@ -55,8 +55,8 @@ const messages = {
'compose.attach.doodle': '落書きをする', 'compose.attach.doodle': '落書きをする',
'compose.attach': 'アタッチ...', 'compose.attach': 'アタッチ...',
'advanced-options.local-only.short': 'ローカル限定', 'advanced_options.local-only.short': 'ローカル限定',
'advanced-options.local-only.long': '他のインスタンスには投稿されません', 'advanced_options.local-only.long': '他のインスタンスには投稿されません',
'advanced_options.icon_title': '高度な設定', 'advanced_options.icon_title': '高度な設定',
}; };

View File

@ -33,11 +33,13 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from 'flavours/glitch/util/uuid'; import uuid from 'flavours/glitch/util/uuid';
import { me } from 'flavours/glitch/util/initial_state'; import { me } from 'flavours/glitch/util/initial_state';
import { overwrite } from 'flavours/glitch/util/js_helpers';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
mounted: false, mounted: false,
advanced_options: ImmutableMap({ advanced_options: ImmutableMap({
do_not_federate: false, do_not_federate: false,
threaded_mode: false,
}), }),
sensitive: false, sensitive: false,
spoiler: false, spoiler: false,
@ -55,6 +57,7 @@ const initialState = ImmutableMap({
suggestions: ImmutableList(), suggestions: ImmutableList(),
default_advanced_options: ImmutableMap({ default_advanced_options: ImmutableMap({
do_not_federate: false, do_not_federate: false,
threaded_mode: null, // Do not reset
}), }),
default_privacy: 'public', default_privacy: 'public',
default_sensitive: false, default_sensitive: false,
@ -83,6 +86,20 @@ function statusToTextMentions(state, status) {
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join(''); 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) { function clearAll(state) {
return state.withMutations(map => { return state.withMutations(map => {
map.set('text', ''); map.set('text', '');
@ -90,7 +107,10 @@ function clearAll(state) {
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('is_submitting', false); map.set('is_submitting', false);
map.set('in_reply_to', null); 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('privacy', state.get('default_privacy'));
map.set('sensitive', false); map.set('sensitive', false);
map.update('media_attachments', list => list.clear()); map.update('media_attachments', list => list.clear());
@ -98,6 +118,31 @@ function clearAll(state) {
}); });
}; };
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) { function appendMedia(state, media) {
const prevSize = state.get('media_attachments').size; const prevSize = state.get('media_attachments').size;
@ -182,8 +227,7 @@ export default function compose(state = initialState, action) {
return state.set('mounted', false); return state.set('mounted', false);
case COMPOSE_ADVANCED_OPTIONS_CHANGE: case COMPOSE_ADVANCED_OPTIONS_CHANGE:
return state return state
.set('advanced_options', .set('advanced_options', state.get('advanced_options').set(action.option, !!overwrite(!state.getIn(['advanced_options', action.option]), action.value)))
state.get('advanced_options').set(action.option, !state.getIn(['advanced_options', action.option])))
.set('idempotencyKey', uuid()); .set('idempotencyKey', uuid());
case COMPOSE_SENSITIVITY_CHANGE: case COMPOSE_SENSITIVITY_CHANGE:
return state.withMutations(map => { return state.withMutations(map => {
@ -220,9 +264,10 @@ export default function compose(state = initialState, action) {
map.set('in_reply_to', action.status.get('id')); map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status)); map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.set('advanced_options', new ImmutableMap({ map.update(
do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')), '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('focusDate', new Date());
map.set('preselectDate', new Date()); map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
@ -243,14 +288,17 @@ export default function compose(state = initialState, action) {
map.set('spoiler', false); map.set('spoiler', false);
map.set('spoiler_text', ''); map.set('spoiler_text', '');
map.set('privacy', state.get('default_privacy')); 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()); map.set('idempotencyKey', uuid());
}); });
case COMPOSE_SUBMIT_REQUEST: case COMPOSE_SUBMIT_REQUEST:
case COMPOSE_UPLOAD_CHANGE_REQUEST: case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_submitting', true); return state.set('is_submitting', true);
case COMPOSE_SUBMIT_SUCCESS: 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_SUBMIT_FAIL:
case COMPOSE_UPLOAD_CHANGE_FAIL: case COMPOSE_UPLOAD_CHANGE_FAIL:
return state.set('is_submitting', false); return state.set('is_submitting', false);

View File

@ -134,6 +134,27 @@
} }
} }
.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 { .composer--textarea--suggestions {
display: block; display: block;
position: absolute; position: absolute;

View File

@ -0,0 +1,5 @@
// 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;
}