diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ace82fc..a774ec4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [Unreleased patch] +### Fixed +- Composer: Forcing the scope to default after settings save. + ## [1.0.0] - 2020-06-15 ### Added - Emoji reactions. @@ -43,5 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial beta release. +[Unreleased]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...develop +[Unreleased patch]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v1.0.0...stable/1.0.x [1.0.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/compare/v0.9.0...v1.0.0 [0.9.0]: https://gitlab.com/soapbox-pub/soapbox-fe/-/tags/v0.9.0 diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index 4f8e7d1f2..a05f2b516 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -7,6 +7,7 @@ export const ME_FETCH_FAIL = 'ME_FETCH_FAIL'; export const ME_FETCH_SKIP = 'ME_FETCH_SKIP'; export const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST'; +export const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS'; export const ME_PATCH_FAIL = 'ME_PATCH_FAIL'; const hasToken = getState => getState().hasIn(['auth', 'user', 'access_token']); @@ -35,7 +36,7 @@ export function patchMe(params) { return api(getState) .patch('/api/v1/accounts/update_credentials', params) .then(response => { - dispatch(fetchMeSuccess(response.data)); + dispatch(patchMeSuccess(response.data)); }).catch(error => { dispatch(patchMeFail(error)); }); @@ -72,6 +73,16 @@ export function patchMeRequest() { }; } +export function patchMeSuccess(me) { + return (dispatch, getState) => { + dispatch(importFetchedAccount(me)); + dispatch({ + type: ME_PATCH_SUCCESS, + me, + }); + }; +} + export function patchMeFail(error) { return { type: ME_PATCH_FAIL, diff --git a/app/soapbox/reducers/__tests__/compose-test.js b/app/soapbox/reducers/__tests__/compose-test.js index 9c13c3e71..a31bab857 100644 --- a/app/soapbox/reducers/__tests__/compose-test.js +++ b/app/soapbox/reducers/__tests__/compose-test.js @@ -1,7 +1,11 @@ import reducer from '../compose'; +import { Map as ImmutableMap } from 'immutable'; +import { COMPOSE_REPLY } from 'soapbox/actions/compose'; +import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; +import { SETTING_CHANGE } from 'soapbox/actions/settings'; describe('compose reducer', () => { - it('should return the initial state', () => { + it('returns the initial state by default', () => { expect(reducer(undefined, {}).toJS()).toMatchObject({ mounted: 0, sensitive: false, @@ -27,4 +31,100 @@ describe('compose reducer', () => { tagHistory: [], }); }); + + it('uses \'public\' scope as default', () => { + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap(), + account: ImmutableMap(), + }; + expect(reducer(undefined, action).toJS()).toMatchObject({ privacy: 'public' }); + }); + + it('uses \'direct\' scope when replying to a DM', () => { + const state = ImmutableMap({ default_privacy: 'public' }); + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap({ visibility: 'direct' }), + account: ImmutableMap(), + }; + expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'direct' }); + }); + + it('uses \'private\' scope when replying to a private post', () => { + const state = ImmutableMap({ default_privacy: 'public' }); + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap({ visibility: 'private' }), + account: ImmutableMap(), + }; + expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'private' }); + }); + + it('uses \'unlisted\' scope when replying to an unlisted post', () => { + const state = ImmutableMap({ default_privacy: 'public' }); + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap({ visibility: 'unlisted' }), + account: ImmutableMap(), + }; + expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' }); + }); + + it('uses \'private\' scope when set as preference and replying to a public post', () => { + const state = ImmutableMap({ default_privacy: 'private' }); + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap({ visibility: 'public' }), + account: ImmutableMap(), + }; + expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'private' }); + }); + + it('uses \'unlisted\' scope when set as preference and replying to a public post', () => { + const state = ImmutableMap({ default_privacy: 'unlisted' }); + const action = { + type: COMPOSE_REPLY, + status: ImmutableMap({ visibility: 'public' }), + account: ImmutableMap(), + }; + expect(reducer(state, action).toJS()).toMatchObject({ privacy: 'unlisted' }); + }); + + it('sets preferred scope on user login', () => { + const state = ImmutableMap({ default_privacy: 'public' }); + const action = { + type: ME_FETCH_SUCCESS, + me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, + }; + expect(reducer(state, action).toJS()).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'unlisted', + }); + }); + + it('sets preferred scope on settings change', () => { + const state = ImmutableMap({ default_privacy: 'public' }); + const action = { + type: SETTING_CHANGE, + path: ['defaultPrivacy'], + value: 'unlisted', + }; + expect(reducer(state, action).toJS()).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'unlisted', + }); + }); + + it('sets default scope on settings save (but retains current scope)', () => { + const state = ImmutableMap({ default_privacy: 'public', privacy: 'public' }); + const action = { + type: ME_PATCH_SUCCESS, + me: { pleroma: { settings_store: { soapbox_fe: { defaultPrivacy: 'unlisted' } } } }, + }; + expect(reducer(state, action).toJS()).toMatchObject({ + default_privacy: 'unlisted', + privacy: 'public', + }); + }); }); diff --git a/app/soapbox/reducers/compose.js b/app/soapbox/reducers/compose.js index d2d1f5961..d9d554d47 100644 --- a/app/soapbox/reducers/compose.js +++ b/app/soapbox/reducers/compose.js @@ -39,7 +39,7 @@ import { import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; import { REDRAFT } from '../actions/statuses'; -import { ME_FETCH_SUCCESS } from '../actions/me'; +import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from '../actions/me'; import { SETTING_CHANGE, FE_NAME } from '../actions/settings'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import uuid from '../uuid'; @@ -199,6 +199,7 @@ const expandMentions = status => { }; export default function compose(state = initialState, action) { + let me, defaultPrivacy; switch(action.type) { case STORE_HYDRATE: return hydrate(state, action.state.get('compose')); @@ -249,7 +250,7 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); - if (action.status.get('spoiler_text').length > 0) { + if (action.status.get('spoiler_text', '').length > 0) { map.set('spoiler', true); map.set('spoiler_text', action.status.get('spoiler_text')); } else { @@ -361,10 +362,15 @@ export default function compose(state = initialState, action) { case COMPOSE_POLL_SETTINGS_CHANGE: return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); case ME_FETCH_SUCCESS: - const me = fromJS(action.me); - const defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy']); + me = fromJS(action.me); + defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy']); if (!defaultPrivacy) return state; return state.set('default_privacy', defaultPrivacy).set('privacy', defaultPrivacy); + case ME_PATCH_SUCCESS: + me = fromJS(action.me); + defaultPrivacy = me.getIn(['pleroma', 'settings_store', FE_NAME, 'defaultPrivacy']); + if (!defaultPrivacy) return state; + return state.set('default_privacy', defaultPrivacy); case SETTING_CHANGE: const pathString = action.path.join(','); if (pathString === 'defaultPrivacy') diff --git a/app/soapbox/reducers/me.js b/app/soapbox/reducers/me.js index a79780196..d5304c4e4 100644 --- a/app/soapbox/reducers/me.js +++ b/app/soapbox/reducers/me.js @@ -1,4 +1,9 @@ -import { ME_FETCH_SUCCESS, ME_FETCH_FAIL, ME_FETCH_SKIP } from '../actions/me'; +import { + ME_FETCH_SUCCESS, + ME_FETCH_FAIL, + ME_FETCH_SKIP, + ME_PATCH_SUCCESS, +} from '../actions/me'; import { AUTH_LOGGED_OUT } from '../actions/auth'; const initialState = null; @@ -6,6 +11,7 @@ const initialState = null; export default function me(state = initialState, action) { switch(action.type) { case ME_FETCH_SUCCESS: + case ME_PATCH_SUCCESS: return action.me.id; case ME_FETCH_FAIL: case ME_FETCH_SKIP: diff --git a/app/soapbox/reducers/meta.js b/app/soapbox/reducers/meta.js index 9d4462c77..6bd397c2c 100644 --- a/app/soapbox/reducers/meta.js +++ b/app/soapbox/reducers/meta.js @@ -1,7 +1,7 @@ 'use strict'; import { STORE_HYDRATE } from '../actions/store'; -import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; +import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; import { Map as ImmutableMap, fromJS } from 'immutable'; const initialState = ImmutableMap(); @@ -11,6 +11,7 @@ export default function meta(state = initialState, action) { case STORE_HYDRATE: return state.merge(action.state.get('meta')); case ME_FETCH_SUCCESS: + case ME_PATCH_SUCCESS: const me = fromJS(action.me); if (me.has('pleroma')) { const pleroPrefs = me.get('pleroma').delete('settings_store'); diff --git a/app/soapbox/reducers/settings.js b/app/soapbox/reducers/settings.js index 15456430e..91b111aee 100644 --- a/app/soapbox/reducers/settings.js +++ b/app/soapbox/reducers/settings.js @@ -3,7 +3,7 @@ import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications'; import { STORE_HYDRATE } from '../actions/store'; import { EMOJI_USE } from '../actions/emojis'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; -import { ME_FETCH_SUCCESS } from 'soapbox/actions/me'; +import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me'; import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from '../uuid'; @@ -32,6 +32,7 @@ export default function settings(state = initialState, action) { case STORE_HYDRATE: return hydrate(state, action.state.get('settings')); case ME_FETCH_SUCCESS: + case ME_PATCH_SUCCESS: const me = fromJS(action.me); const fePrefs = me.getIn(['pleroma', 'settings_store', FE_NAME]); return state.merge(fePrefs);