diff --git a/app/soapbox/actions/__tests__/statuses-test.js b/app/soapbox/actions/__tests__/statuses-test.js index 972330f39..463b87437 100644 --- a/app/soapbox/actions/__tests__/statuses-test.js +++ b/app/soapbox/actions/__tests__/statuses-test.js @@ -1,8 +1,13 @@ +import { fromJS, Map as ImmutableMap } from 'immutable'; + import { STATUSES_IMPORT } from 'soapbox/actions/importer'; import { __stub } from 'soapbox/api'; import { mockStore, rootState } from 'soapbox/jest/test-helpers'; +import { normalizeStatus } from 'soapbox/normalizers/status'; +import rootReducer from 'soapbox/reducers'; import { fetchContext } from '../statuses'; +import { deleteStatus } from '../statuses'; describe('fetchContext()', () => { it('handles Mitra context', done => { @@ -25,3 +30,133 @@ describe('fetchContext()', () => { }).catch(console.error); }); }); + +describe('deleteStatus()', () => { + let store; + + describe('if logged out', () => { + beforeEach(() => { + const state = rootReducer(undefined, {}).set('me', null); + store = mockStore(state); + }); + + it('should do nothing', async() => { + await store.dispatch(deleteStatus('1', {})); + const actions = store.getActions(); + + expect(actions).toEqual([]); + }); + }); + + describe('if logged in', () => { + const statusId = 'AHU2RrX0wdcwzCYjFQ'; + const cachedStatus = normalizeStatus({ + id: statusId, + }); + + beforeEach(() => { + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('statuses', fromJS({ + [statusId]: cachedStatus, + })); + store = mockStore(state); + }); + + describe('with a successful API request', () => { + let status; + + beforeEach(() => { + status = require('soapbox/__fixtures__/pleroma-status-deleted.json'); + + __stub((mock) => { + mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status); + }); + }); + + it('should delete the status from the API', async() => { + const expectedActions = [ + { + type: 'STATUS_DELETE_REQUEST', + params: cachedStatus, + }, + { type: 'STATUS_DELETE_SUCCESS', id: statusId }, + { + type: 'TIMELINE_DELETE', + id: statusId, + accountId: null, + references: ImmutableMap({}), + reblogOf: null, + }, + ]; + await store.dispatch(deleteStatus(statusId, {})); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + + it('should handle redraft', async() => { + const expectedActions = [ + { + type: 'STATUS_DELETE_REQUEST', + params: cachedStatus, + }, + { type: 'STATUS_DELETE_SUCCESS', id: statusId }, + { + type: 'TIMELINE_DELETE', + id: statusId, + accountId: null, + references: ImmutableMap({}), + reblogOf: null, + }, + { + type: 'COMPOSE_SET_STATUS', + status: cachedStatus, + rawText: status.text, + explicitAddressing: false, + spoilerText: '', + contentType: 'text/markdown', + v: { + build: undefined, + compatVersion: '0.0.0', + software: 'Mastodon', + version: '0.0.0', + }, + withRedraft: true, + }, + { type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined }, + ]; + await store.dispatch(deleteStatus(statusId, {}, true)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with an unsuccessful API request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onDelete(`/api/v1/statuses/${statusId}`).networkError(); + }); + }); + + it('should dispatch failed action', async() => { + const expectedActions = [ + { + type: 'STATUS_DELETE_REQUEST', + params: cachedStatus, + }, + { + type: 'STATUS_DELETE_FAIL', + params: cachedStatus, + error: new Error('Network Error'), + }, + ]; + await store.dispatch(deleteStatus(statusId, {}, true)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + }); +}); diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 7363f979d..bc580455a 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -124,7 +124,7 @@ export function fetchStatus(id) { export function deleteStatus(id, routerHistory, withRedraft = false) { return (dispatch, getState) => { - if (!isLoggedIn(getState)) return; + if (!isLoggedIn(getState)) return null; let status = getState().getIn(['statuses', id]); @@ -132,19 +132,22 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { status = status.set('poll', getState().getIn(['polls', status.get('poll')])); } - dispatch({ type: STATUS_DELETE_REQUEST, id }); + dispatch({ type: STATUS_DELETE_REQUEST, params: status }); - api(getState).delete(`/api/v1/statuses/${id}`).then(response => { - dispatch({ type: STATUS_DELETE_SUCCESS, id }); - dispatch(deleteFromTimelines(id)); + return api(getState) + .delete(`/api/v1/statuses/${id}`) + .then(response => { + dispatch({ type: STATUS_DELETE_SUCCESS, id }); + dispatch(deleteFromTimelines(id)); - if (withRedraft) { - dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type, withRedraft)); - dispatch(openModal('COMPOSE')); - } - }).catch(error => { - dispatch({ type: STATUS_DELETE_FAIL, id, error }); - }); + if (withRedraft) { + dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type, withRedraft)); + dispatch(openModal('COMPOSE')); + } + }) + .catch(error => { + dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); + }); }; } diff --git a/app/soapbox/reducers/__tests__/statuses-test.js b/app/soapbox/reducers/__tests__/statuses-test.js index 91c960308..fb245062a 100644 --- a/app/soapbox/reducers/__tests__/statuses-test.js +++ b/app/soapbox/reducers/__tests__/statuses-test.js @@ -8,6 +8,8 @@ import { STATUS_IMPORT } from 'soapbox/actions/importer'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_FAIL, + STATUS_DELETE_REQUEST, + STATUS_DELETE_FAIL, } from 'soapbox/actions/statuses'; import reducer from '../statuses'; @@ -183,4 +185,56 @@ Promoting free speech, even for people and ideas you dislike`; expect(result).toEqual(4); }); }); + + describe('STATUS_DELETE_REQUEST', () => { + it('decrements the replies_count of its parent', () => { + const state = fromJS({ '123': { replies_count: 4 } }); + + const action = { + type: STATUS_DELETE_REQUEST, + params: { in_reply_to_id: '123' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(3); + }); + + it('gracefully does nothing if no parent', () => { + const state = fromJS({ '123': { replies_count: 4 } }); + + const action = { + type: STATUS_DELETE_REQUEST, + params: { id: '1' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(4); + }); + }); + + describe('STATUS_DELETE_FAIL', () => { + it('decrements the replies_count of its parent', () => { + const state = fromJS({ '123': { replies_count: 4 } }); + + const action = { + type: STATUS_DELETE_FAIL, + params: { in_reply_to_id: '123' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(5); + }); + + it('gracefully does nothing if no parent', () => { + const state = fromJS({ '123': { replies_count: 4 } }); + + const action = { + type: STATUS_DELETE_FAIL, + params: { id: '1' }, + }; + + const result = reducer(state, action).getIn(['123', 'replies_count']); + expect(result).toEqual(4); + }); + }); }); diff --git a/app/soapbox/reducers/statuses.ts b/app/soapbox/reducers/statuses.ts index e35468b4f..aac7dc975 100644 --- a/app/soapbox/reducers/statuses.ts +++ b/app/soapbox/reducers/statuses.ts @@ -28,6 +28,8 @@ import { STATUS_UNMUTE_SUCCESS, STATUS_REVEAL, STATUS_HIDE, + STATUS_DELETE_REQUEST, + STATUS_DELETE_FAIL, } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -152,7 +154,7 @@ const deleteStatus = (state: State, id: string, references: Array) => { return state.delete(id); }; -const importPendingStatus = (state: State, { in_reply_to_id }: APIEntity) => { +const incrementReplyCount = (state: State, { in_reply_to_id }: APIEntity) => { if (in_reply_to_id) { return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => { return typeof count === 'number' ? count + 1 : 0; @@ -162,7 +164,7 @@ const importPendingStatus = (state: State, { in_reply_to_id }: APIEntity) => { } }; -const deletePendingStatus = (state: State, { in_reply_to_id }: APIEntity) => { +const decrementReplyCount = (state: State, { in_reply_to_id }: APIEntity) => { if (in_reply_to_id) { return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => { return typeof count === 'number' ? Math.max(0, count - 1) : 0; @@ -200,9 +202,9 @@ export default function statuses(state = initialState, action: AnyAction): State case STATUSES_IMPORT: return importStatuses(state, action.statuses, action.expandSpoilers); case STATUS_CREATE_REQUEST: - return importPendingStatus(state, action.params); + return incrementReplyCount(state, action.params); case STATUS_CREATE_FAIL: - return deletePendingStatus(state, action.params); + return decrementReplyCount(state, action.params); case FAVOURITE_REQUEST: return simulateFavourite(state, action.status.id, true); case UNFAVOURITE_REQUEST: @@ -249,6 +251,10 @@ export default function statuses(state = initialState, action: AnyAction): State } }); }); + case STATUS_DELETE_REQUEST: + return decrementReplyCount(state, action.params); + case STATUS_DELETE_FAIL: + return incrementReplyCount(state, action.params); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); default: