diff options
Diffstat (limited to 'app/javascript/flavours')
11 files changed, 170 insertions, 71 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 261c72b2a..baa98e98f 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -75,6 +75,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; 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.' }, @@ -88,6 +90,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => { } }; +export function setComposeToStatus(status, text, spoiler_text) { + return{ + type: COMPOSE_SET_STATUS, + status, + text, + spoiler_text, + }; +}; + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -150,8 +161,9 @@ export function directCompose(account, routerHistory) { 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'], '') : ''; @@ -159,20 +171,25 @@ export function submitCompose(routerHistory) { 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']), }, @@ -202,14 +219,16 @@ export function submitCompose(routerHistory) { } }; - 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) { diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 7db357df1..6ffcf181d 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -2,7 +2,7 @@ import api from 'flavours/glitch/util/api'; 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'; @@ -26,6 +26,10 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; 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, @@ -81,6 +85,37 @@ export function redraft(status, raw_text, content_type) { }; }; +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]); diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index ae67c6116..d553c2413 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -13,6 +13,7 @@ import classNames from 'classnames'; 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}' }, @@ -126,6 +127,10 @@ class StatusActionBar extends ImmutablePureComponent { 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); } @@ -225,6 +230,7 @@ class StatusActionBar extends ImmutablePureComponent { } 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 { diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index bc3c43d85..358b89ab9 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -17,7 +17,7 @@ import { 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'; @@ -169,6 +169,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status.get('id'), history)); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 5dfc119c1..47dc87f16 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -47,6 +47,7 @@ class ComposeForm extends ImmutablePureComponent { preselectDate: PropTypes.instanceOf(Date), isSubmitting: PropTypes.bool, isChangingUpload: PropTypes.bool, + isEditing: PropTypes.bool, isUploading: PropTypes.bool, onChange: PropTypes.func, onSubmit: PropTypes.func, @@ -293,6 +294,7 @@ class ComposeForm extends ImmutablePureComponent { spoilerText, suggestions, spoilersAlwaysOn, + isEditing, } = this.props; const countText = this.getFulltextForCharacterCounting(); @@ -364,6 +366,7 @@ class ComposeForm extends ImmutablePureComponent { <Publisher countText={countText} disabled={!this.canSubmit()} + isEditing={isEditing} onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} privacy={privacy} diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index 1531dcaa9..9a8c0f510 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -2,7 +2,7 @@ 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'; @@ -23,6 +23,7 @@ const messages = defineMessages({ defaultMessage: '{publish}!', id: 'compose_form.publish_loud', }, + saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, }); export default @injectIntl @@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent { onSubmit: PropTypes.func, privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), + isEditing: PropTypes.bool, }; handleSubmit = () => { @@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent { }; 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', { @@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent { 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} diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index 8eff8a36b..a037bbbcc 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -51,6 +51,7 @@ function mapStateToProps (state) { 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']), diff --git a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js index 395a9aa5b..dd6899be4 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js @@ -1,14 +1,24 @@ 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 => ({ diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index eb4583026..33ff0ce7e 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -11,6 +11,7 @@ import classNames from 'classnames'; 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' }, @@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent { 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, @@ -84,6 +86,10 @@ class ActionBar extends React.PureComponent { 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); } @@ -166,6 +172,7 @@ class ActionBar extends React.PureComponent { 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 { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 12ea407ad..653fabeae 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -26,7 +26,7 @@ import { 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'; @@ -307,6 +307,10 @@ class Status extends ImmutablePureComponent { } } + handleEditClick = (status, history) => { + this.props.dispatch(editStatus(status.get('id'), history)); + } + handleDirectClick = (account, router) => { this.props.dispatch(directCompose(account, router)); } @@ -585,6 +589,7 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} + onEdit={this.handleEditClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index d2ea0a924..f97c799e7 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -46,6 +46,7 @@ import { 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'; @@ -75,6 +76,7 @@ const initialState = ImmutableMap({ spoiler: false, spoiler_text: '', privacy: null, + id: null, content_type: defaultContentType || 'text/plain', text: '', focusDate: null, @@ -160,6 +162,7 @@ function apiStatusToTextHashtags (state, status) { 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); @@ -400,6 +403,7 @@ export default function compose(state = initialState, action) { .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'))); @@ -434,6 +438,7 @@ export default function compose(state = initialState, action) { 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', @@ -573,6 +578,34 @@ export default function compose(state = initialState, action) { })); } }); + 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')), + multiple: action.status.getIn(['poll', 'multiple']), + expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), + })); + } + }); case COMPOSE_POLL_ADD: return state.set('poll', initialPoll); case COMPOSE_POLL_REMOVE: |