export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
+export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
+
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
}
};
+export function setComposeToStatus(status, text, spoiler_text) {
+ return{
+ type: COMPOSE_SET_STATUS,
+ status,
+ text,
+ spoiler_text,
+ };
+};
+
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
- let status = getState().getIn(['compose', 'text'], '');
- let media = getState().getIn(['compose', 'media_attachments']);
+ let status = getState().getIn(['compose', 'text'], '');
+ const media = getState().getIn(['compose', 'media_attachments']);
+ const statusId = getState().getIn(['compose', 'id'], null);
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
return;
}
- dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
}
- api(getState).post('/api/v1/statuses', {
- status,
- content_type: getState().getIn(['compose', 'content_type']),
- in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
- media_ids: media.map(item => item.get('id')),
- sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
- spoiler_text: spoilerText,
- visibility: getState().getIn(['compose', 'privacy']),
- poll: getState().getIn(['compose', 'poll'], null),
- }, {
+
+ dispatch(submitComposeRequest());
+
+ api(getState).request({
+ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
+ method: statusId === null ? 'post' : 'put',
+ data: {
+ status,
+ content_type: getState().getIn(['compose', 'content_type']),
+ in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
+ media_ids: media.map(item => item.get('id')),
+ sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
+ spoiler_text: spoilerText,
+ visibility: getState().getIn(['compose', 'privacy']),
+ poll: getState().getIn(['compose', 'poll'], null),
+ },
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}
};
- insertIfOnline('home');
+ if (statusId === null) {
+ insertIfOnline('home');
+ }
- if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+ if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
if (!response.data.local_only) {
insertIfOnline('public');
}
- } else if (response.data.visibility === 'direct') {
+ } else if (statusId === null && response.data.visibility === 'direct') {
insertIfOnline('direct');
}
}).catch(function (error) {
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
-import { ensureComposeIsVisible } from './compose';
+import { ensureComposeIsVisible, setComposeToStatus } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const REDRAFT = 'REDRAFT';
+export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
+export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
+export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
+
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
};
};
+export const editStatus = (id, routerHistory) => (dispatch, getState) => {
+ let status = getState().getIn(['statuses', id]);
+
+ if (status.get('poll')) {
+ status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+ }
+
+ dispatch(fetchStatusSourceRequest());
+
+ api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
+ dispatch(fetchStatusSourceSuccess());
+ ensureComposeIsVisible(getState, routerHistory);
+ dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
+ }).catch(error => {
+ dispatch(fetchStatusSourceFail(error));
+ });
+};
+
+export const fetchStatusSourceRequest = () => ({
+ type: STATUS_FETCH_SOURCE_REQUEST,
+});
+
+export const fetchStatusSourceSuccess = () => ({
+ type: STATUS_FETCH_SOURCE_SUCCESS,
+});
+
+export const fetchStatusSourceFail = error => ({
+ type: STATUS_FETCH_SOURCE_FAIL,
+ error,
+});
+
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+ edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
this.props.onDelete(this.props.status, this.context.router.history, true);
}
+ handleEditClick = () => {
+ this.props.onEdit(this.props.status, this.context.router.history);
+ }
+
handlePinClick = () => {
this.props.onPin(this.props.status);
}
}
if (writtenByMe) {
+ menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
-import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
}
},
+ onEdit (status, history) {
+ dispatch(editStatus(status.get('id'), history));
+ },
+
onDirect (account, router) {
dispatch(directCompose(account, router));
},
preselectDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isChangingUpload: PropTypes.bool,
+ isEditing: PropTypes.bool,
isUploading: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
spoilerText,
suggestions,
spoilersAlwaysOn,
+ isEditing,
} = this.props;
const countText = this.getFulltextForCharacterCounting();
<Publisher
countText={countText}
disabled={!this.canSubmit()}
+ isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
import { length } from 'stringz';
import ImmutablePureComponent from 'react-immutable-pure-component';
defaultMessage: '{publish}!',
id: 'compose_form.publish_loud',
},
+ saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
});
export default @injectIntl
onSubmit: PropTypes.func,
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
+ isEditing: PropTypes.bool,
};
handleSubmit = () => {
};
render () {
- const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
+ const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
const diff = maxChars - length(countText || '');
const computedClass = classNames('composer--publisher', {
over: diff < 0,
});
+ const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
+
+ let publishText;
+ if (isEditing) {
+ publishText = intl.formatMessage(messages.saveChanges);
+ } else if (privacy === 'private' || privacy === 'direct') {
+ const iconId = privacyIcons[privacy];
+ publishText = (
+ <span>
+ <Icon id={iconId} /> {intl.formatMessage(messages.publish)}
+ </span>
+ );
+ } else {
+ publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
+ }
+
return (
<div className={computedClass}>
- {sideArm && sideArm !== 'none' ? (
+ {sideArm && !isEditing && sideArm !== 'none' ? (
<Button
className='side_arm'
disabled={disabled}
onClick={onSecondarySubmit}
style={{ padding: null }}
- text={
- <span>
- <Icon
- id={{
- public: 'globe',
- unlisted: 'unlock',
- private: 'lock',
- direct: 'envelope',
- }[sideArm]}
- />
- </span>
- }
+ text={<Icon id={privacyIcons[sideArm]} />}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
/>
) : null}
<Button
className='primary'
- text={function () {
- switch (true) {
- case !!sideArm && sideArm !== 'none':
- case privacy === 'direct':
- case privacy === 'private':
- return (
- <span>
- <Icon
- id={{
- direct: 'envelope',
- private: 'lock',
- public: 'globe',
- unlisted: 'unlock',
- }[privacy]}
- />
- {' '}
- <FormattedMessage {...messages.publish} />
- </span>
- );
- case privacy === 'public':
- return (
- <span>
- <FormattedMessage
- {...messages.publishLoud}
- values={{ publish: <FormattedMessage {...messages.publish} /> }}
- />
- </span>
- );
- default:
- return <span><FormattedMessage {...messages.publish} /></span>;
- }
- }()}
+ text={publishText}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
onClick={this.handleSubmit}
disabled={disabled}
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
+ isEditing: state.getIn(['compose', 'id']) !== null,
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
layout: state.getIn(['local_settings', 'layout']),
import { connect } from 'react-redux';
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
-import { makeGetStatus } from 'flavours/glitch/selectors';
import ReplyIndicator from '../components/reply_indicator';
-function makeMapStateToProps (state) {
- const inReplyTo = state.getIn(['compose', 'in_reply_to']);
+const makeMapStateToProps = () => {
+ const mapStateToProps = state => {
+ let statusId = state.getIn(['compose', 'id'], null);
+ let editing = true;
- return {
- status: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null,
+ if (statusId === null) {
+ statusId = state.getIn(['compose', 'in_reply_to']);
+ editing = false;
+ }
+
+ return {
+ status: state.getIn(['statuses', statusId]),
+ editing,
+ };
};
+
+ return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+ edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
onMuteConversation: PropTypes.func,
onBlock: PropTypes.func,
onDelete: PropTypes.func.isRequired,
+ onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onReport: PropTypes.func,
this.props.onDelete(this.props.status, this.context.router.history, true);
}
+ handleEditClick = () => {
+ this.props.onEdit(this.props.status, this.context.router.history);
+ }
+
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
+ menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
directCompose,
} from 'flavours/glitch/actions/compose';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
}
}
+ handleEditClick = (status, history) => {
+ this.props.dispatch(editStatus(status.get('id'), history));
+ }
+
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
+ onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
INIT_MEDIA_EDIT_MODAL,
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
COMPOSE_CHANGE_MEDIA_FOCUS,
+ COMPOSE_SET_STATUS,
} from 'flavours/glitch/actions/compose';
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
spoiler: false,
spoiler_text: '',
privacy: null,
+ id: null,
content_type: defaultContentType || 'text/plain',
text: '',
focusDate: null,
function clearAll(state) {
return state.withMutations(map => {
+ map.set('id', null);
map.set('text', '');
if (defaultContentType) map.set('content_type', defaultContentType);
map.set('spoiler', false);
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
case COMPOSE_REPLY:
return state.withMutations(map => {
+ map.set('id', null);
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('spoiler', false);
map.set('spoiler_text', '');
map.set('privacy', state.get('default_privacy'));
+ map.set('id', null);
map.set('poll', null);
map.update(
'advanced_options',
map.set('spoiler_text', '');
}
+ if (action.status.get('poll')) {
+ map.set('poll', ImmutableMap({
+ options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+ multiple: action.status.getIn(['poll', 'multiple']),
+ expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
+ }));
+ }
+ });
+ case COMPOSE_SET_STATUS:
+ return state.withMutations(map => {
+ map.set('id', action.status.get('id'));
+ map.set('text', action.text);
+ map.set('in_reply_to', action.status.get('in_reply_to_id'));
+ map.set('privacy', action.status.get('visibility'));
+ map.set('media_attachments', action.status.get('media_attachments'));
+ map.set('focusDate', new Date());
+ map.set('caretPosition', null);
+ map.set('idempotencyKey', uuid());
+ map.set('sensitive', action.status.get('sensitive'));
+
+ if (action.spoiler_text.length > 0) {
+ map.set('spoiler', true);
+ map.set('spoiler_text', action.spoiler_text);
+ } else {
+ map.set('spoiler', false);
+ map.set('spoiler_text', '');
+ }
+
if (action.status.get('poll')) {
map.set('poll', ImmutableMap({
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),