diff --git a/package.json b/package.json index 1049e74bc..67ca75aae 100644 --- a/package.json +++ b/package.json @@ -90,8 +90,6 @@ "@types/semver": "^7.3.9", "@webbtc/webln-types": "^3.0.0", "autoprefixer": "^10.4.15", - "axios": "^1.2.2", - "axios-mock-adapter": "^1.22.0", "blurhash": "^2.0.0", "bowser": "^2.11.0", "browserslist": "^4.16.6", diff --git a/src/actions/about.ts b/src/actions/about.ts index 124698c1a..0735856d9 100644 --- a/src/actions/about.ts +++ b/src/actions/about.ts @@ -12,7 +12,8 @@ const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dis const filename = `${slug}${locale ? `.${locale}` : ''}.html`; return api(getState).get(`/instance/about/${filename}`) - .then(({ data: html }) => { + .then((response) => response.text()) + .then((html) => { dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html }); return html; }) diff --git a/src/actions/account-notes.test.ts b/src/actions/account-notes.test.ts deleted file mode 100644 index 6641fe39b..000000000 --- a/src/actions/account-notes.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; - -import { submitAccountNote } from './account-notes.ts'; - -describe('submitAccountNote()', () => { - let store: ReturnType; - - beforeEach(() => { - store = mockStore(rootState); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost('/api/v1/accounts/1/note').reply(200, {}); - }); - }); - - it('post the note to the API', async() => { - const expectedActions = [ - { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, - { type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} }, - ]; - await store.dispatch(submitAccountNote('1', 'hello')); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost('/api/v1/accounts/1/note').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' }, - { - type: 'ACCOUNT_NOTE_SUBMIT_FAIL', - error: new Error('Network Error'), - }, - ]; - await store.dispatch(submitAccountNote('1', 'hello')); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); diff --git a/src/actions/account-notes.ts b/src/actions/account-notes.ts index ea69ccc13..4a989b842 100644 --- a/src/actions/account-notes.ts +++ b/src/actions/account-notes.ts @@ -15,8 +15,9 @@ const submitAccountNote = (id: string, value: string) => .post(`/api/v1/accounts/${id}/note`, { comment: value, }) - .then(response => { - dispatch(submitAccountNoteSuccess(response.data)); + .then((response) => response.json()) + .then((data) => { + dispatch(submitAccountNoteSuccess(data)); }) .catch(error => dispatch(submitAccountNoteFail(error))); }; diff --git a/src/actions/accounts.test.ts b/src/actions/accounts.test.ts deleted file mode 100644 index 3c8befcaa..000000000 --- a/src/actions/accounts.test.ts +++ /dev/null @@ -1,1518 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildInstance, buildRelationship } from 'soapbox/jest/factory.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; -import { normalizeAccount } from 'soapbox/normalizers/index.ts'; -import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists.ts'; - -import { - authorizeFollowRequest, - blockAccount, - createAccount, - expandFollowers, - expandFollowing, - expandFollowRequests, - fetchAccount, - fetchAccountByUsername, - fetchFollowers, - fetchFollowing, - fetchFollowRequests, - fetchRelationships, - muteAccount, - removeFromFollowers, - subscribeAccount, - unblockAccount, - unmuteAccount, - unsubscribeAccount, -} from './accounts.ts'; - -let store: ReturnType; - -describe('createAccount()', () => { - const params = { - email: 'foo@bar.com', - }; - - describe('with a successful API request', () => { - beforeEach(() => { - const state = rootState; - store = mockStore(state); - - __stub((mock) => { - mock.onPost('/api/v1/accounts').reply(200, { token: '123 ' }); - }); - }); - - it('dispatches the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_CREATE_REQUEST', params }, - { - type: 'ACCOUNT_CREATE_SUCCESS', - params, - token: { token: '123 ' }, - }, - ]; - await store.dispatch(createAccount(params)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); - -describe('fetchAccount()', () => { - const id = '123'; - - describe('when the account has "should_refetch" set to false', () => { - beforeEach(() => { - const account = normalizeAccount({ - id, - acct: 'justin-username', - display_name: 'Justin L', - avatar: 'test.jpg', - }); - - const state = { - ...rootState, - entities: { - 'ACCOUNTS': { - store: { - [id]: account, - }, - lists: {}, - }, - }, - }; - - store = mockStore(state); - - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}`).reply(200, account); - }); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', async () => { - const account = await import('soapbox/__fixtures__/pleroma-account.json'); - - beforeEach(() => { - const state = rootState; - store = mockStore(state); - - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}`).reply(200, account); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_FETCH_REQUEST', id: '123' }, - { type: 'ACCOUNTS_IMPORT', accounts: [account] }, - { - type: 'ACCOUNT_FETCH_SUCCESS', - account, - }, - ]; - - await store.dispatch(fetchAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - const state = rootState; - store = mockStore(state); - - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_FETCH_REQUEST', id: '123' }, - { - type: 'ACCOUNT_FETCH_FAIL', - id, - error: new Error('Network Error'), - skipAlert: true, - }, - ]; - - await store.dispatch(fetchAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); - -describe('fetchAccountByUsername()', () => { - const id = '123'; - const username = 'tiger'; - let state, account: any; - - beforeEach(() => { - account = normalizeAccount({ - id, - acct: username, - display_name: 'Tiger', - avatar: 'test.jpg', - birthday: undefined, - }); - - state = { - ...rootState, - entities: { - 'ACCOUNTS': { - store: { - [id]: account, - }, - lists: {}, - }, - }, - }; - - store = mockStore(state); - - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}`).reply(200, account); - }); - }); - - describe('when "accountByUsername" feature is enabled', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - instance: buildInstance({ - version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)', - pleroma: { - metadata: { - features: [], - }, - }, - }), - }; - - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${username}`).reply(200, account); - mock.onGet(`/api/v1/accounts/relationships?${[account.id].map(id => `id[]=${id}`).join('&')}`); - }); - }); - - it('should return dispatch the proper actions', async() => { - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions[0]).toEqual({ - type: 'RELATIONSHIPS_FETCH_REQUEST', - ids: ['123'], - skipLoading: true, - }); - expect(actions[1].type).toEqual('ACCOUNTS_IMPORT'); - expect(actions[2].type).toEqual('ACCOUNT_FETCH_SUCCESS'); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${username}`).networkError(); - }); - }); - - it('should return dispatch the proper actions', async() => { - const expectedActions = [ - { - type: 'ACCOUNT_FETCH_FAIL', - id: null, - error: new Error('Network Error'), - skipAlert: true, - }, - { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username: 'tiger' }, - ]; - - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); - - describe('when "accountLookup" feature is enabled', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - instance: buildInstance({ - version: '3.4.1 (compatible; TruthSocial 1.0.0)', - pleroma: { - metadata: { - features: [], - }, - }, - }), - }; - - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/lookup').reply(200, account); - }); - }); - - it('should return dispatch the proper actions', async() => { - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions[0]).toEqual({ - type: 'ACCOUNT_LOOKUP_REQUEST', - acct: username, - }); - expect(actions[1].type).toEqual('ACCOUNTS_IMPORT'); - expect(actions[2].type).toEqual('ACCOUNT_LOOKUP_SUCCESS'); - expect(actions[3].type).toEqual('RELATIONSHIPS_FETCH_REQUEST'); - expect(actions[4].type).toEqual('ACCOUNT_FETCH_SUCCESS'); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/lookup').networkError(); - }); - }); - - it('should return dispatch the proper actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_LOOKUP_REQUEST', acct: 'tiger' }, - { type: 'ACCOUNT_LOOKUP_FAIL' }, - { - type: 'ACCOUNT_FETCH_FAIL', - id: null, - error: new Error('Network Error'), - skipAlert: true, - }, - { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username }, - ]; - - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); - - describe('when using the accountSearch function', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/search').reply(200, [account]); - }); - }); - - it('should return dispatch the proper actions', async() => { - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions[0]).toEqual({ - type: 'ACCOUNT_SEARCH_REQUEST', - params: { q: username, limit: 5, resolve: true }, - }); - expect(actions[1].type).toEqual('ACCOUNTS_IMPORT'); - expect(actions[2].type).toEqual('ACCOUNT_SEARCH_SUCCESS'); - expect(actions[3]).toEqual({ - type: 'RELATIONSHIPS_FETCH_REQUEST', - ids: [ '123' ], - skipLoading: true, - }); - expect(actions[4].type).toEqual('ACCOUNT_FETCH_SUCCESS'); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/search').networkError(); - }); - }); - - it('should return dispatch the proper actions', async() => { - const expectedActions = [ - { - type: 'ACCOUNT_SEARCH_REQUEST', - params: { q: username, limit: 5, resolve: true }, - }, - { type: 'ACCOUNT_SEARCH_FAIL', skipAlert: true }, - { - type: 'ACCOUNT_FETCH_FAIL', - id: null, - error: new Error('Network Error'), - skipAlert: true, - }, - { type: 'ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP', username }, - ]; - - await store.dispatch(fetchAccountByUsername(username)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('blockAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(blockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/block`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_BLOCK_REQUEST', id }, - { - type: 'ACCOUNT_BLOCK_SUCCESS', - relationship: {}, - statuses: ImmutableMap({}), - }, - ]; - await store.dispatch(blockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/block`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_BLOCK_REQUEST', id }, - { type: 'ACCOUNT_BLOCK_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(blockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('unblockAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(unblockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/unblock`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNBLOCK_REQUEST', id }, - { - type: 'ACCOUNT_UNBLOCK_SUCCESS', - relationship: {}, - }, - ]; - await store.dispatch(unblockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/unblock`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNBLOCK_REQUEST', id }, - { type: 'ACCOUNT_UNBLOCK_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(unblockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('muteAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(unblockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/mute`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_MUTE_REQUEST', id }, - { - type: 'ACCOUNT_MUTE_SUCCESS', - relationship: {}, - statuses: ImmutableMap({}), - }, - ]; - await store.dispatch(muteAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/mute`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_MUTE_REQUEST', id }, - { type: 'ACCOUNT_MUTE_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(muteAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('unmuteAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(unblockAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/unmute`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNMUTE_REQUEST', id }, - { - type: 'ACCOUNT_UNMUTE_SUCCESS', - relationship: {}, - }, - ]; - await store.dispatch(unmuteAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/unmute`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNMUTE_REQUEST', id }, - { type: 'ACCOUNT_UNMUTE_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(unmuteAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('subscribeAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(subscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/pleroma/accounts/${id}/subscribe`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_SUBSCRIBE_REQUEST', id }, - { - type: 'ACCOUNT_SUBSCRIBE_SUCCESS', - relationship: {}, - }, - ]; - await store.dispatch(subscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/pleroma/accounts/${id}/subscribe`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_SUBSCRIBE_REQUEST', id }, - { type: 'ACCOUNT_SUBSCRIBE_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(subscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('unsubscribeAccount()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(subscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/pleroma/accounts/${id}/unsubscribe`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNSUBSCRIBE_REQUEST', id }, - { - type: 'ACCOUNT_UNSUBSCRIBE_SUCCESS', - relationship: {}, - }, - ]; - await store.dispatch(unsubscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/pleroma/accounts/${id}/unsubscribe`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_UNSUBSCRIBE_REQUEST', id }, - { type: 'ACCOUNT_UNSUBSCRIBE_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(unsubscribeAccount(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('removeFromFollowers()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(removeFromFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).reply(200, {}); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id }, - { - type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS', - relationship: {}, - }, - ]; - await store.dispatch(removeFromFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/accounts/${id}/remove_from_followers`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST', id }, - { type: 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(removeFromFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('fetchFollowers()', () => { - const id = '1'; - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}/followers`).reply(200, [], { - link: `; rel='prev'`, - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWERS_FETCH_REQUEST', id }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOWERS_FETCH_SUCCESS', - id, - accounts: [], - next: null, - }, - ]; - await store.dispatch(fetchFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}/followers`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWERS_FETCH_REQUEST', id }, - { type: 'FOLLOWERS_FETCH_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(fetchFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('expandFollowers()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - followers: ImmutableMap({ - [id]: ListRecord({ - next: 'next_url', - }), - }), - }), - }; - - store = mockStore(state); - }); - - describe('when the url is null', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - followers: ImmutableMap({ - [id]: ListRecord({ - next: null, - }), - }), - }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').reply(200, [], { - link: `; rel='prev'`, - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWERS_EXPAND_REQUEST', id }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOWERS_EXPAND_SUCCESS', - id, - accounts: [], - next: null, - }, - ]; - await store.dispatch(expandFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWERS_EXPAND_REQUEST', id }, - { type: 'FOLLOWERS_EXPAND_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(expandFollowers(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('fetchFollowing()', () => { - const id = '1'; - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}/following`).reply(200, [], { - link: `; rel='prev'`, - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWING_FETCH_REQUEST', id }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOWING_FETCH_SUCCESS', - id, - accounts: [], - next: null, - }, - ]; - await store.dispatch(fetchFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${id}/following`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWING_FETCH_REQUEST', id }, - { type: 'FOLLOWING_FETCH_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(fetchFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('expandFollowing()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - following: ImmutableMap({ - [id]: ListRecord({ - next: 'next_url', - }), - }), - }), - }; - - store = mockStore(state); - }); - - describe('when the url is null', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - following: ImmutableMap({ - [id]: ListRecord({ - next: null, - }), - }), - }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').reply(200, [], { - link: `; rel='prev'`, - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWING_EXPAND_REQUEST', id }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOWING_EXPAND_SUCCESS', - id, - accounts: [], - next: null, - }, - ]; - await store.dispatch(expandFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOWING_EXPAND_REQUEST', id }, - { type: 'FOLLOWING_EXPAND_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(expandFollowing(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('fetchRelationships()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchRelationships([id])); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('without newAccountIds', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - relationships: ImmutableMap({ [id]: buildRelationship() }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchRelationships([id])); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - relationships: ImmutableMap(), - }; - - store = mockStore(state); - - __stub((mock) => { - mock - .onGet(`/api/v1/accounts/relationships?${[id].map(id => `id[]=${id}`).join('&')}`) - .reply(200, []); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'RELATIONSHIPS_FETCH_REQUEST', ids: [id], skipLoading: true }, - { - type: 'RELATIONSHIPS_FETCH_SUCCESS', - relationships: [], - skipLoading: true, - }, - ]; - await store.dispatch(fetchRelationships([id])); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock - .onGet(`/api/v1/accounts/relationships?${[id].map(id => `id[]=${id}`).join('&')}`) - .networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'RELATIONSHIPS_FETCH_REQUEST', ids: [id], skipLoading: true }, - { type: 'RELATIONSHIPS_FETCH_FAIL', skipLoading: true, error: new Error('Network Error') }, - ]; - await store.dispatch(fetchRelationships([id])); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('fetchFollowRequests()', () => { - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - relationships: ImmutableMap(), - }; - - store = mockStore(state); - - __stub((mock) => { - mock.onGet('/api/v1/follow_requests').reply(200, [], { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOW_REQUESTS_FETCH_SUCCESS', - accounts: [], - next: null, - }, - ]; - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/follow_requests').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_FETCH_REQUEST' }, - { type: 'FOLLOW_REQUESTS_FETCH_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(fetchFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('expandFollowRequests()', () => { - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - follow_requests: ListRecord({ - next: 'next_url', - }), - }), - }; - store = mockStore(state); - }); - - describe('when the url is null', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '123', - user_lists: ReducerRecord({ - follow_requests: ListRecord({ - next: null, - }), - }), - }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').reply(200, [], { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [] }, - { - type: 'FOLLOW_REQUESTS_EXPAND_SUCCESS', - accounts: [], - next: null, - }, - ]; - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('next_url').networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUESTS_EXPAND_REQUEST' }, - { type: 'FOLLOW_REQUESTS_EXPAND_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(expandFollowRequests()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('authorizeFollowRequest()', () => { - const id = '1'; - - describe('when logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('when logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '123' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/follow_requests/${id}/authorize`).reply(200); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id }, - { type: 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS', id }, - ]; - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onPost(`/api/v1/follow_requests/${id}/authorize`).networkError(); - }); - }); - - it('should dispatch the correct actions', async() => { - const expectedActions = [ - { type: 'FOLLOW_REQUEST_AUTHORIZE_REQUEST', id }, - { type: 'FOLLOW_REQUEST_AUTHORIZE_FAIL', id, error: new Error('Network Error') }, - ]; - await store.dispatch(authorizeFollowRequest(id)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 7ccaf6e5c..b4a3c49fa 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -1,10 +1,11 @@ +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import { importEntities } from 'soapbox/entity-store/actions.ts'; import { Entities } from 'soapbox/entity-store/entities.ts'; import { selectAccount } from 'soapbox/selectors/index.ts'; import { isLoggedIn } from 'soapbox/utils/auth.ts'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedAccount, @@ -12,7 +13,6 @@ import { importErrorWhileFetchingAccountByUsername, } from './importer/index.ts'; -import type { AxiosError, CancelToken } from 'axios'; import type { Map as ImmutableMap } from 'immutable'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity, Status } from 'soapbox/types/entities.ts'; @@ -118,7 +118,7 @@ const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST'; const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS'; const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL'; -const maybeRedirectLogin = (error: AxiosError, history?: History) => { +const maybeRedirectLogin = (error: HTTPError, history?: History) => { // The client is unauthorized - redirect to login. if (history && error?.response?.status === 401) { history.push('/login'); @@ -130,7 +130,7 @@ const noOp = () => new Promise(f => f(undefined)); const createAccount = (params: Record) => async (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); - return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => { + return api(getState, 'app').post('/api/v1/accounts', params).then((response) => response.json()).then((token) => { return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token }); }).catch(error => { dispatch({ type: ACCOUNT_CREATE_FAIL, error, params }); @@ -152,9 +152,10 @@ const fetchAccount = (id: string) => return api(getState) .get(`/api/v1/accounts/${id}`) - .then(response => { - dispatch(importFetchedAccount(response.data)); - dispatch(fetchAccountSuccess(response.data)); + .then((response) => response.json()) + .then((data) => { + dispatch(importFetchedAccount(data)); + dispatch(fetchAccountSuccess(data)); }) .catch(error => { dispatch(fetchAccountFail(id, error)); @@ -167,10 +168,10 @@ const fetchAccountByUsername = (username: string, history?: History) => const features = getFeatures(instance); if (features.accountByUsername && (me || !features.accountLookup)) { - return api(getState).get(`/api/v1/accounts/${username}`).then(response => { - dispatch(fetchRelationships([response.data.id])); - dispatch(importFetchedAccount(response.data)); - dispatch(fetchAccountSuccess(response.data)); + return api(getState).get(`/api/v1/accounts/${username}`).then((response) => response.json()).then((data) => { + dispatch(fetchRelationships([data.id])); + dispatch(importFetchedAccount(data)); + dispatch(fetchAccountSuccess(data)); }).catch(error => { dispatch(fetchAccountFail(null, error)); dispatch(importErrorWhileFetchingAccountByUsername(username)); @@ -230,10 +231,10 @@ const blockAccount = (id: string) => return api(getState) .post(`/api/v1/accounts/${id}/block`) - .then(response => { - dispatch(importEntities([response.data], Entities.RELATIONSHIPS)); + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - return dispatch(blockAccountSuccess(response.data, getState().statuses)); + return dispatch(blockAccountSuccess(data, getState().statuses)); }).catch(error => dispatch(blockAccountFail(error))); }; @@ -245,9 +246,9 @@ const unblockAccount = (id: string) => return api(getState) .post(`/api/v1/accounts/${id}/unblock`) - .then(response => { - dispatch(importEntities([response.data], Entities.RELATIONSHIPS)); - return dispatch(unblockAccountSuccess(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + return dispatch(unblockAccountSuccess(data)); }) .catch(error => dispatch(unblockAccountFail(error))); }; @@ -307,10 +308,10 @@ const muteAccount = (id: string, notifications?: boolean, duration = 0) => return api(getState) .post(`/api/v1/accounts/${id}/mute`, params) - .then(response => { - dispatch(importEntities([response.data], Entities.RELATIONSHIPS)); + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - return dispatch(muteAccountSuccess(response.data, getState().statuses)); + return dispatch(muteAccountSuccess(data, getState().statuses)); }) .catch(error => dispatch(muteAccountFail(error))); }; @@ -323,9 +324,9 @@ const unmuteAccount = (id: string) => return api(getState) .post(`/api/v1/accounts/${id}/unmute`) - .then(response => { - dispatch(importEntities([response.data], Entities.RELATIONSHIPS)); - return dispatch(unmuteAccountSuccess(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(importEntities([data], Entities.RELATIONSHIPS)); + return dispatch(unmuteAccountSuccess(data)); }) .catch(error => dispatch(unmuteAccountFail(error))); }; @@ -369,7 +370,7 @@ const subscribeAccount = (id: string, notifications?: boolean) => return api(getState) .post(`/api/v1/pleroma/accounts/${id}/subscribe`, { notifications }) - .then(response => dispatch(subscribeAccountSuccess(response.data))) + .then((response) => response.json()).then((data) => dispatch(subscribeAccountSuccess(data))) .catch(error => dispatch(subscribeAccountFail(error))); }; @@ -381,7 +382,7 @@ const unsubscribeAccount = (id: string) => return api(getState) .post(`/api/v1/pleroma/accounts/${id}/unsubscribe`) - .then(response => dispatch(unsubscribeAccountSuccess(response.data))) + .then((response) => response.json()).then((data) => dispatch(unsubscribeAccountSuccess(data))) .catch(error => dispatch(unsubscribeAccountFail(error))); }; @@ -423,7 +424,7 @@ const removeFromFollowers = (id: string) => return api(getState) .post(`/api/v1/accounts/${id}/remove_from_followers`) - .then(response => dispatch(removeFromFollowersSuccess(response.data))) + .then((response) => response.json()).then((data) => dispatch(removeFromFollowersSuccess(data))) .catch(error => dispatch(removeFromFollowersFail(id, error))); }; @@ -449,12 +450,13 @@ const fetchFollowers = (id: string) => return api(getState) .get(`/api/v1/accounts/${id}/followers`) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowersSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }) .catch(error => { dispatch(fetchFollowersFail(id, error)); @@ -493,12 +495,13 @@ const expandFollowers = (id: string) => return api(getState) .get(url) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowersSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }) .catch(error => { dispatch(expandFollowersFail(id, error)); @@ -529,12 +532,13 @@ const fetchFollowing = (id: string) => return api(getState) .get(`/api/v1/accounts/${id}/following`) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowingSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }) .catch(error => { dispatch(fetchFollowingFail(id, error)); @@ -573,12 +577,13 @@ const expandFollowing = (id: string) => return api(getState) .get(url) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowingSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }) .catch(error => { dispatch(expandFollowingFail(id, error)); @@ -618,9 +623,9 @@ const fetchRelationships = (accountIds: string[]) => return api(getState) .get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`) - .then(response => { - dispatch(importEntities(response.data, Entities.RELATIONSHIPS)); - dispatch(fetchRelationshipsSuccess(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(importEntities(data, Entities.RELATIONSHIPS)); + dispatch(fetchRelationshipsSuccess(data)); }) .catch(error => dispatch(fetchRelationshipsFail(error))); }; @@ -651,10 +656,11 @@ const fetchFollowRequests = () => return api(getState) .get('/api/v1/follow_requests') - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchFollowRequestsSuccess(data, next)); }) .catch(error => dispatch(fetchFollowRequestsFail(error))); }; @@ -688,10 +694,11 @@ const expandFollowRequests = () => return api(getState) .get(url) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(expandFollowRequestsSuccess(data, next)); }) .catch(error => dispatch(expandFollowRequestsFail(error))); }; @@ -773,8 +780,8 @@ const pinAccount = (id: string) => dispatch(pinAccountRequest(id)); - return api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { - dispatch(pinAccountSuccess(response.data)); + return api(getState).post(`/api/v1/accounts/${id}/pin`).then((response) => response.json()).then((data) => { + dispatch(pinAccountSuccess(data)); }).catch(error => { dispatch(pinAccountFail(error)); }); @@ -786,8 +793,8 @@ const unpinAccount = (id: string) => dispatch(unpinAccountRequest(id)); - return api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { - dispatch(unpinAccountSuccess(response.data)); + return api(getState).post(`/api/v1/accounts/${id}/unpin`).then((response) => response.json()).then((data) => { + dispatch(unpinAccountSuccess(data)); }).catch(error => { dispatch(unpinAccountFail(error)); }); @@ -796,7 +803,7 @@ const unpinAccount = (id: string) => const updateNotificationSettings = (params: Record) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params }); - return api(getState).put('/api/pleroma/notification_settings', params).then(({ data }) => { + return api(getState).put('/api/pleroma/notification_settings', params).then((response) => response.json()).then((data) => { dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data }); }).catch(error => { dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error }); @@ -838,9 +845,9 @@ const fetchPinnedAccounts = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchPinnedAccountsRequest(id)); - api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => { - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchPinnedAccountsSuccess(id, response.data, null)); + api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchPinnedAccountsSuccess(id, data, null)); }).catch(error => { dispatch(fetchPinnedAccountsFail(id, error)); }); @@ -867,7 +874,7 @@ const fetchPinnedAccountsFail = (id: string, error: unknown) => ({ const accountSearch = (params: Record, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ACCOUNT_SEARCH_REQUEST, params }); - return api(getState).get('/api/v1/accounts/search', { params, signal }).then(({ data: accounts }) => { + return api(getState).get('/api/v1/accounts/search', { searchParams: params, signal }).then((response) => response.json()).then((accounts) => { dispatch(importFetchedAccounts(accounts)); dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts }); return accounts; @@ -877,10 +884,10 @@ const accountSearch = (params: Record, signal?: AbortSignal) => }); }; -const accountLookup = (acct: string, cancelToken?: CancelToken) => +const accountLookup = (acct: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct }); - return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => { + return api(getState).get('/api/v1/accounts/lookup', { searchParams: { acct }, signal }).then((response) => response.json()).then((account) => { if (account && account.id) dispatch(importFetchedAccount(account)); dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account }); return account; @@ -898,11 +905,11 @@ const fetchBirthdayReminders = (month: number, day: number) => dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me }); - return api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => { - dispatch(importFetchedAccounts(response.data)); + return api(getState).get('/api/v1/pleroma/birthdays', { searchParams: { day, month } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); dispatch({ type: BIRTHDAY_REMINDERS_FETCH_SUCCESS, - accounts: response.data, + accounts: data, day, month, id: me, diff --git a/src/actions/admin.ts b/src/actions/admin.ts index 2383aec95..3b465252b 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -3,9 +3,8 @@ import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } fr import { accountIdsToAccts } from 'soapbox/selectors/index.ts'; import { filterBadges, getTagDiff } from 'soapbox/utils/badges.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; -import type { AxiosResponse } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity } from 'soapbox/types/entities.ts'; @@ -74,7 +73,7 @@ const fetchConfig = () => dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); return api(getState) .get('/api/v1/pleroma/admin/config') - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); }).catch(error => { dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error }); @@ -86,7 +85,7 @@ const updateConfig = (configs: Record[]) => dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs }); return api(getState) .post('/api/v1/pleroma/admin/config', { configs }) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot }); }).catch(error => { dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs }); @@ -111,7 +110,8 @@ function fetchReports(params: Record = {}) { dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params }); try { - const { data: reports } = await api(getState).get('/api/v1/admin/reports', { params }); + const response = await api(getState).get('/api/v1/admin/reports', { searchParams: params }); + const reports = await response.json(); reports.forEach((report: APIEntity) => { dispatch(importFetchedAccount(report.account?.account)); dispatch(importFetchedAccount(report.target_account?.account)); @@ -158,8 +158,9 @@ function fetchUsers(filters: Record, page = 1, query?: string | }; try { - const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params }); - const next = getLinks(response as AxiosResponse).refs.find(link => link.rel === 'next')?.uri; + const response = await api(getState).get(url || '/api/v1/admin/accounts', { searchParams: params }); + const accounts = await response.json(); + const next = response.next(); dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account))); dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id))); @@ -206,8 +207,9 @@ const deleteUser = (accountId: string) => const nicknames = accountIdsToAccts(getState(), [accountId]); dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountId }); return api(getState) - .delete('/api/v1/pleroma/admin/users', { data: { nicknames } }) - .then(({ data: nicknames }) => { + .request('DELETE', '/api/v1/pleroma/admin/users', { nicknames }) + .then((response) => response.json()) + .then(({ nicknames }) => { dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountId }); }).catch(error => { dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountId }); @@ -218,8 +220,9 @@ function approveUser(accountId: string) { return async (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId }); try { - const { data: user } = await api(getState) - .post(`/api/v1/admin/accounts/${accountId}/approve`); + const { user } = await api(getState) + .post(`/api/v1/admin/accounts/${accountId}/approve`) + .then((response) => response.json()); dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId }); } catch (error) { dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId }); @@ -231,8 +234,9 @@ function rejectUser(accountId: string) { return async (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId }); try { - const { data: user } = await api(getState) - .post(`/api/v1/admin/accounts/${accountId}/reject`); + const { user } = await api(getState) + .post(`/api/v1/admin/accounts/${accountId}/reject`) + .then((response) => response.json()); dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId }); } catch (error) { dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId }); @@ -288,7 +292,7 @@ const untagUsers = (accountIds: string[], tags: string[]) => dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags }); return api(getState) - .delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } }) + .request('DELETE', '/api/v1/pleroma/admin/users/tag', { nicknames, tags }) .then(() => { dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags }); }).catch(error => { @@ -320,7 +324,7 @@ const addPermission = (accountIds: string[], permissionGroup: string) => dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) .post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data }); }).catch(error => { dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup }); @@ -332,8 +336,8 @@ const removePermission = (accountIds: string[], permissionGroup: string) => const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) - .delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } }) - .then(({ data }) => { + .request('DELETE', `/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) + .then((response) => response.json()).then((data) => { dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data }); }).catch(error => { dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup }); diff --git a/src/actions/aliases.ts b/src/actions/aliases.ts index 3706c6415..b4df15e48 100644 --- a/src/actions/aliases.ts +++ b/src/actions/aliases.ts @@ -45,8 +45,8 @@ const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchAliasesRequest()); api(getState).get('/api/pleroma/aliases') - .then(response => { - dispatch(fetchAliasesSuccess(response.data.aliases)); + .then((response) => response.json()).then((data) => { + dispatch(fetchAliasesSuccess(data.aliases)); }) .catch(err => dispatch(fetchAliasesFail(err))); }; @@ -75,7 +75,7 @@ const fetchAliasesSuggestions = (q: string) => limit: 4, }; - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + api(getState).get('/api/v1/accounts/search', { searchParams: params }).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data)); dispatch(fetchAliasesSuggestionsReady(q, data)); }).catch(error => toast.showAlertForError(error)); @@ -111,11 +111,12 @@ const addToAliases = (account: Account) => dispatch(addToAliasesRequest()); api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma?.ap_id] }) - .then((response => { + .then((response) => response.json()) + .then((data) => { toast.success(messages.createSuccess); dispatch(addToAliasesSuccess); - dispatch(patchMeSuccess(response.data)); - })) + dispatch(patchMeSuccess(data)); + }) .catch(err => dispatch(addToAliasesFail(err))); return; @@ -162,10 +163,10 @@ const removeFromAliases = (account: string) => dispatch(removeFromAliasesRequest()); api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter((id: string) => id !== account) }) - .then(response => { + .then((response) => response.json()).then((data) => { toast.success(messages.removeSuccess); dispatch(removeFromAliasesSuccess); - dispatch(patchMeSuccess(response.data)); + dispatch(patchMeSuccess(data)); }) .catch(err => dispatch(removeFromAliasesFail(err))); @@ -174,12 +175,10 @@ const removeFromAliases = (account: string) => dispatch(addToAliasesRequest()); - api(getState).delete('/api/pleroma/aliases', { - data: { - alias: account, - }, + api(getState).request('DELETE', '/api/pleroma/aliases', { + alias: account, }) - .then(response => { + .then(() => { toast.success(messages.removeSuccess); dispatch(removeFromAliasesSuccess); dispatch(fetchAliases); diff --git a/src/actions/apps.ts b/src/actions/apps.ts index e593f418e..39d1664f4 100644 --- a/src/actions/apps.ts +++ b/src/actions/apps.ts @@ -21,7 +21,7 @@ export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL'; export function createApp(params?: Record, baseURL?: string) { return (dispatch: React.Dispatch) => { dispatch({ type: APP_CREATE_REQUEST, params }); - return baseClient(null, baseURL).post('/api/v1/apps', params).then(({ data: app }) => { + return baseClient(null, baseURL).post('/api/v1/apps', params).then((response) => response.json()).then((app) => { dispatch({ type: APP_CREATE_SUCCESS, params, app }); return app as Record; }).catch(error => { @@ -34,7 +34,7 @@ export function createApp(params?: Record, baseURL?: string) { export function verifyAppCredentials(token: string) { return (dispatch: React.Dispatch) => { dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token }); - return baseClient(token).get('/api/v1/apps/verify_credentials').then(({ data: app }) => { + return baseClient(token).get('/api/v1/apps/verify_credentials').then((response) => response.json()).then((app) => { dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app }); return app; }).catch(error => { diff --git a/src/actions/auth.ts b/src/actions/auth.ts index 08ecb51fe..079024bc2 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -14,6 +14,7 @@ import { createApp } from 'soapbox/actions/apps.ts'; import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me.ts'; import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth.ts'; import { startOnboarding } from 'soapbox/actions/onboarding.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import { custom } from 'soapbox/custom.ts'; import { queryClient } from 'soapbox/queries/client.ts'; import { selectAccount } from 'soapbox/selectors/index.ts'; @@ -28,7 +29,6 @@ import api, { baseClient } from '../api/index.ts'; import { importFetchedAccount } from './importer/index.ts'; -import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; @@ -132,7 +132,7 @@ export const otpVerify = (code: string, mfa_token: string) => challenge_type: 'totp', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', scope: getScopes(getState()), - }).then(({ data: token }) => dispatch(authLoggedIn(token))); + }).then((response) => response.json()).then((token) => dispatch(authLoggedIn(token))); }; export const verifyCredentials = (token: string, accountUrl?: string) => { @@ -141,7 +141,7 @@ export const verifyCredentials = (token: string, accountUrl?: string) => { return (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token }); - return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => { + return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then((response) => response.json()).then((account) => { dispatch(importFetchedAccount(account)); dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); if (account.id === getState().me) dispatch(fetchMeSuccess(account)); @@ -149,7 +149,7 @@ export const verifyCredentials = (token: string, accountUrl?: string) => { }).catch(error => { if (error?.response?.status === 403 && error?.response?.data?.id) { // The user is waitlisted - const account = error.response.data; + const account = error.data; dispatch(importFetchedAccount(account)); dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); if (account.id === getState().me) dispatch(fetchMeSuccess(account)); @@ -163,18 +163,31 @@ export const verifyCredentials = (token: string, accountUrl?: string) => { }; }; +export class MfaRequiredError extends Error { + + constructor(public token: string) { + super('MFA is required'); + } + +} + export const logIn = (username: string, password: string) => (dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => { return dispatch(createUserToken(normalizeUsername(username), password)); - }).catch((error: AxiosError) => { - if ((error.response?.data as any)?.error === 'mfa_required') { - // If MFA is required, throw the error and handle it in the component. - throw error; - } else if ((error.response?.data as any)?.identifier === 'awaiting_approval') { - toast.error(messages.awaitingApproval); - } else { - // Return "wrong password" message. - toast.error(messages.invalidCredentials); + }).catch(async (error) => { + if (error instanceof HTTPError) { + const data = await error.response.error(); + if (data) { + if (data.error === 'mfa_required' && 'mfa_token' in data && typeof data.mfa_token === 'string') { + // If MFA is required, throw the error and handle it in the component. + throw new MfaRequiredError(data.mfa_token); + } else if (data.error === 'awaiting_approval') { + toast.error(messages.awaitingApproval); + } else { + // Return "wrong password" message. + toast.error(messages.invalidCredentials); + } + } } throw error; }); diff --git a/src/actions/backups.ts b/src/actions/backups.ts index dfabb1a85..0693e8172 100644 --- a/src/actions/backups.ts +++ b/src/actions/backups.ts @@ -13,7 +13,7 @@ export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL'; export const fetchBackups = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: BACKUPS_FETCH_REQUEST }); - return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => + return api(getState).get('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) => dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }), ).catch(error => { dispatch({ type: BACKUPS_FETCH_FAIL, error }); @@ -23,7 +23,7 @@ export const fetchBackups = () => export const createBackup = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: BACKUPS_CREATE_REQUEST }); - return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => + return api(getState).post('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) => dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }), ).catch(error => { dispatch({ type: BACKUPS_CREATE_FAIL, error }); diff --git a/src/actions/blocks.test.ts b/src/actions/blocks.test.ts deleted file mode 100644 index c0a2418d8..000000000 --- a/src/actions/blocks.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; -import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists.ts'; - -import { expandBlocks, fetchBlocks } from './blocks.ts'; - -const account = { - acct: 'twoods', - display_name: 'Tiger Woods', - id: '22', - username: 'twoods', -}; - -describe('fetchBlocks()', () => { - let store: ReturnType; - - describe('if logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(fetchBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('if logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '1234' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const blocks = await import('soapbox/__fixtures__/blocks.json'); - - __stub((mock) => { - mock.onGet('/api/v1/blocks').reply(200, blocks, { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should fetch blocks from the API', async() => { - const expectedActions = [ - { type: 'BLOCKS_FETCH_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [account] }, - { type: 'BLOCKS_FETCH_SUCCESS', accounts: [account], next: null }, - { - type: 'RELATIONSHIPS_FETCH_REQUEST', - ids: ['22'], - skipLoading: true, - }, - ]; - await store.dispatch(fetchBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/blocks').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'BLOCKS_FETCH_REQUEST' }, - { type: 'BLOCKS_FETCH_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(fetchBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); - -describe('expandBlocks()', () => { - let store: ReturnType; - - describe('if logged out', () => { - beforeEach(() => { - const state = { ...rootState, me: null }; - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('if logged in', () => { - beforeEach(() => { - const state = { ...rootState, me: '1234' }; - store = mockStore(state); - }); - - describe('without a url', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '1234', - user_lists: UserListsRecord({ blocks: ListRecord({ next: null }) }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a url', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '1234', - user_lists: UserListsRecord({ blocks: ListRecord({ next: 'example' }) }), - }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const blocks = await import('soapbox/__fixtures__/blocks.json'); - - __stub((mock) => { - mock.onGet('example').reply(200, blocks, { - link: '; rel=\'prev\'', - }); - }); - }); - - it('should fetch blocks from the url', async() => { - const expectedActions = [ - { type: 'BLOCKS_EXPAND_REQUEST' }, - { type: 'ACCOUNTS_IMPORT', accounts: [account] }, - { type: 'BLOCKS_EXPAND_SUCCESS', accounts: [account], next: null }, - { - type: 'RELATIONSHIPS_FETCH_REQUEST', - ids: ['22'], - skipLoading: true, - }, - ]; - await store.dispatch(expandBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('example').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'BLOCKS_EXPAND_REQUEST' }, - { type: 'BLOCKS_EXPAND_FAIL', error: new Error('Network Error') }, - ]; - await store.dispatch(expandBlocks()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); - }); -}); diff --git a/src/actions/blocks.ts b/src/actions/blocks.ts index bada74f2a..f58d19998 100644 --- a/src/actions/blocks.ts +++ b/src/actions/blocks.ts @@ -1,6 +1,6 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchRelationships } from './accounts.ts'; import { importFetchedAccounts } from './importer/index.ts'; @@ -22,11 +22,12 @@ const fetchBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => return api(getState) .get('/api/v1/blocks') - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchBlocksSuccess(data, next)); + dispatch(fetchRelationships(data.map((item: any) => item.id)) as any); }) .catch(error => dispatch(fetchBlocksFail(error))); }; @@ -63,11 +64,12 @@ const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => return api(getState) .get(url) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); + .then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(expandBlocksSuccess(data, next)); + dispatch(fetchRelationships(data.map((item: any) => item.id)) as any); }) .catch(error => dispatch(expandBlocksFail(error))); }; diff --git a/src/actions/bookmarks.ts b/src/actions/bookmarks.ts index d37f7fb73..fdf6c9237 100644 --- a/src/actions/bookmarks.ts +++ b/src/actions/bookmarks.ts @@ -1,4 +1,4 @@ -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedStatuses } from './importer/index.ts'; @@ -23,10 +23,11 @@ const fetchBookmarkedStatuses = () => dispatch(fetchBookmarkedStatusesRequest()); - return api(getState).get('/api/v1/bookmarks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - return dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + return api(getState).get('/api/v1/bookmarks').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + return dispatch(fetchBookmarkedStatusesSuccess(data, next)); }).catch(error => { dispatch(fetchBookmarkedStatusesFail(error)); }); @@ -58,10 +59,11 @@ const expandBookmarkedStatuses = () => dispatch(expandBookmarkedStatusesRequest()); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - return dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + return dispatch(expandBookmarkedStatusesSuccess(data, next)); }).catch(error => { dispatch(expandBookmarkedStatusesFail(error)); }); diff --git a/src/actions/chats.ts b/src/actions/chats.ts index 6027530e3..3143e0707 100644 --- a/src/actions/chats.ts +++ b/src/actions/chats.ts @@ -3,7 +3,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { getSettings, changeSetting } from 'soapbox/actions/settings.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { History } from 'soapbox/types/history.ts'; @@ -38,22 +38,19 @@ const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL'; const fetchChatsV1 = () => (dispatch: AppDispatch, getState: () => RootState) => - api(getState).get('/api/v1/pleroma/chats').then((response) => { - dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data }); + api(getState).get('/api/v1/pleroma/chats').then((response) => response.json()).then((data) => { + dispatch({ type: CHATS_FETCH_SUCCESS, chats: data }); }).catch(error => { dispatch({ type: CHATS_FETCH_FAIL, error }); }); const fetchChatsV2 = () => (dispatch: AppDispatch, getState: () => RootState) => - api(getState).get('/api/v2/pleroma/chats').then((response) => { - let next: { uri: string } | undefined = getLinks(response).refs.find(link => link.rel === 'next'); + api(getState).get('/api/v2/pleroma/chats').then(async (response) => { + const next = response.next(); + const data = await response.json(); - if (!next && response.data.length) { - next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` }; - } - - dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null }); + dispatch({ type: CHATS_FETCH_SUCCESS, chats: data, next }); }).catch(error => { dispatch({ type: CHATS_FETCH_FAIL, error }); }); @@ -81,10 +78,11 @@ const expandChats = () => } dispatch({ type: CHATS_EXPAND_REQUEST }); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch({ type: CHATS_EXPAND_SUCCESS, chats: response.data, next: next ? next.uri : null }); + dispatch({ type: CHATS_EXPAND_SUCCESS, chats: data, next }); }).catch(error => { dispatch({ type: CHATS_EXPAND_FAIL, error }); }); @@ -93,7 +91,8 @@ const expandChats = () => const fetchChatMessages = (chatId: string, maxId: string | null = null) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId }); - return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => { + const searchParams = maxId ? { max_id: maxId } : undefined; + return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { searchParams }).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data }); }).catch(error => { dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error }); @@ -105,7 +104,7 @@ const sendChatMessage = (chatId: string, params: Record) => const uuid = `末_${Date.now()}_${crypto.randomUUID()}`; const me = getState().me; dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me }); - return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => { + return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid }); }).catch(error => { dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid }); @@ -164,7 +163,7 @@ const toggleMainWindow = () => const fetchChat = (chatId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CHAT_FETCH_REQUEST, chatId }); - return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => { + return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); }).catch(error => { dispatch({ type: CHAT_FETCH_FAIL, chatId, error }); @@ -174,7 +173,7 @@ const fetchChat = (chatId: string) => const startChat = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CHAT_FETCH_REQUEST, accountId }); - return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => { + return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_FETCH_SUCCESS, chat: data }); return data; }).catch(error => { @@ -191,7 +190,7 @@ const markChatRead = (chatId: string, lastReadId?: string | null) => if (!lastReadId) return; dispatch({ type: CHAT_READ_REQUEST, chatId, lastReadId }); - api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then(({ data }) => { + api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_READ_SUCCESS, chat: data, lastReadId }); }).catch(error => { dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId }); @@ -201,7 +200,7 @@ const markChatRead = (chatId: string, lastReadId?: string | null) => const deleteChatMessage = (chatId: string, messageId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId }); - api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => { + api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then((response) => response.json()).then((data) => { dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data }); }).catch(error => { dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error }); diff --git a/src/actions/compose.ts b/src/actions/compose.ts index aa92dda7e..7bce18894 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -1,8 +1,8 @@ -import axios, { Canceler } from 'axios'; import { throttle } from 'es-toolkit'; import { List as ImmutableList } from 'immutable'; import { defineMessages, IntlShape } from 'react-intl'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import api from 'soapbox/api/index.ts'; import { isNativeEmoji } from 'soapbox/features/emoji/index.ts'; import emojiSearch from 'soapbox/features/emoji/search.ts'; @@ -29,9 +29,7 @@ import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity, Status, Tag } from 'soapbox/types/entities.ts'; import type { History } from 'soapbox/types/history.ts'; -const { CancelToken, isCancel } = axios; - -let cancelFetchComposeSuggestions: Canceler; +let cancelFetchComposeSuggestions: AbortController | undefined; const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; @@ -369,9 +367,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; const media = getState().compose.get(composeId)?.media_attachments; - const progress = new Array(files.length).fill(0); - let total = Array.from(files).reduce((a, v) => a + v.size, 0); - + const progress: number[] = new Array(files.length).fill(0); const mediaCount = media ? media.size : 0; if (files.length + mediaCount > attachmentLimit) { @@ -389,11 +385,10 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => intl, (data) => dispatch(uploadComposeSuccess(composeId, data, f)), (error) => dispatch(uploadComposeFail(composeId, error)), - ({ loaded }: any) => { - progress[i] = loaded; - dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total)); + (e: ProgressEvent) => { + progress[i] = e.loaded; + dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), e.total)); }, - (value) => total += value, )); }); @@ -433,8 +428,8 @@ const changeUploadCompose = (composeId: string, id: string, params: Record { - dispatch(changeUploadComposeSuccess(composeId, response.data)); + dispatch(updateMedia(id, params)).then((response) => response.json()).then((data) => { + dispatch(changeUploadComposeSuccess(composeId, data)); }).catch(error => { dispatch(changeUploadComposeFail(composeId, id, error)); }); @@ -480,9 +475,8 @@ const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolea }); const clearComposeSuggestions = (composeId: string) => { - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions(); - } + cancelFetchComposeSuggestions?.abort(); + return { type: COMPOSE_SUGGESTIONS_CLEAR, id: composeId, @@ -490,23 +484,20 @@ const clearComposeSuggestions = (composeId: string) => { }; const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => { - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions(composeId); - } + cancelFetchComposeSuggestions?.abort(); + api(getState).get('/api/v1/accounts/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestions = cancel; - }), - params: { + signal: cancelFetchComposeSuggestions?.signal, + searchParams: { q: token.slice(1), resolve: false, limit: 10, }, - }).then(response => { - dispatch(importFetchedAccounts(response.data)); - dispatch(readyComposeSuggestionsAccounts(composeId, token, response.data)); + }).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(readyComposeSuggestionsAccounts(composeId, token, data)); }).catch(error => { - if (!isCancel(error)) { + if (error instanceof HTTPError) { toast.showAlertForError(error); } }); @@ -519,9 +510,7 @@ const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, composeId: string, }; const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => { - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions(composeId); - } + cancelFetchComposeSuggestions?.abort(); const state = getState(); @@ -535,18 +524,16 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root } api(getState).get('/api/v2/search', { - cancelToken: new CancelToken(cancel => { - cancelFetchComposeSuggestions = cancel; - }), - params: { + signal: cancelFetchComposeSuggestions?.signal, + searchParams: { q: token.slice(1), limit: 10, type: 'hashtags', }, - }).then(response => { - dispatch(updateSuggestionTags(composeId, token, response.data?.hashtags.map(normalizeTag))); + }).then((response) => response.json()).then((data) => { + dispatch(updateSuggestionTags(composeId, token, data?.hashtags.map(normalizeTag))); }).catch(error => { - if (!isCancel(error)) { + if (error instanceof HTTPError) { toast.showAlertForError(error); } }); diff --git a/src/actions/consumer-auth.ts b/src/actions/consumer-auth.ts index 27828d5fe..1b88677b9 100644 --- a/src/actions/consumer-auth.ts +++ b/src/actions/consumer-auth.ts @@ -1,5 +1,3 @@ -import axios from 'axios'; - import * as BuildConfig from 'soapbox/build-config.ts'; import { isURL } from 'soapbox/utils/auth.ts'; import sourceCode from 'soapbox/utils/code.ts'; @@ -36,17 +34,12 @@ export const prepareRequest = (provider: string) => { localStorage.setItem('soapbox:external:baseurl', baseURL); localStorage.setItem('soapbox:external:scopes', scopes); - const params = { - provider, - authorization: { - client_id, - redirect_uri, - scope: scopes, - }, - }; + const query = new URLSearchParams({ provider }); - const formdata = axios.toFormData(params); - const query = new URLSearchParams(formdata as any); + // FIXME: I don't know if this is the correct way to encode the query params. + query.append('authorization.client_id', client_id); + query.append('authorization.redirect_uri', redirect_uri); + query.append('authorization.scope', scopes); location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`; }; diff --git a/src/actions/conversations.ts b/src/actions/conversations.ts index 6f7727404..650d1bf6f 100644 --- a/src/actions/conversations.ts +++ b/src/actions/conversations.ts @@ -1,6 +1,6 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedAccounts, @@ -53,13 +53,14 @@ const expandConversations = ({ maxId }: Record = {}) => (dispatch: const isLoadingRecent = !!params.since_id; - api(getState).get('/api/v1/conversations', { params }) - .then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + api(getState).get('/api/v1/conversations', { searchParams: params }) + .then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data.reduce((aggr: Array, item: APIEntity) => aggr.concat(item.accounts), []))); - dispatch(importFetchedStatuses(response.data.map((item: Record) => item.last_status).filter((x?: APIEntity) => !!x))); - dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent)); + dispatch(importFetchedAccounts(data.reduce((aggr: Array, item: APIEntity) => aggr.concat(item.accounts), []))); + dispatch(importFetchedStatuses(data.map((item: Record) => item.last_status).filter((x?: APIEntity) => !!x))); + dispatch(expandConversationsSuccess(data, next, isLoadingRecent)); }) .catch(err => dispatch(expandConversationsFail(err))); }; diff --git a/src/actions/directory.ts b/src/actions/directory.ts index 2f9e92bc0..ac29b88ed 100644 --- a/src/actions/directory.ts +++ b/src/actions/directory.ts @@ -18,7 +18,7 @@ const fetchDirectory = (params: Record) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchDirectoryRequest()); - api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { + api(getState).get('/api/v1/directory', { searchParams: { ...params, limit: 20 } }).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data)); dispatch(fetchDirectorySuccess(data)); dispatch(fetchRelationships(data.map((x: APIEntity) => x.id))); @@ -45,7 +45,7 @@ const expandDirectory = (params: Record) => const loadedItems = getState().user_lists.directory.items.size; - api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { + api(getState).get('/api/v1/directory', { searchParams: { ...params, offset: loadedItems, limit: 20 } }).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data)); dispatch(expandDirectorySuccess(data)); dispatch(fetchRelationships(data.map((x: APIEntity) => x.id))); diff --git a/src/actions/domain-blocks.ts b/src/actions/domain-blocks.ts index 3aa4f6c55..62dea3762 100644 --- a/src/actions/domain-blocks.ts +++ b/src/actions/domain-blocks.ts @@ -1,7 +1,7 @@ import { Entities } from 'soapbox/entity-store/entities.ts'; import { isLoggedIn } from 'soapbox/utils/auth.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import type { EntityStore } from 'soapbox/entity-store/types.ts'; import type { Account } from 'soapbox/schemas/index.ts'; @@ -61,13 +61,10 @@ const unblockDomain = (domain: string) => dispatch(unblockDomainRequest(domain)); - // Do it both ways for maximum compatibility - const params = { - params: { domain }, - data: { domain }, - }; + const data = new FormData(); + data.append('domain', domain); - api(getState).delete('/api/v1/domain_blocks', params).then(() => { + api(getState).request('DELETE', '/api/v1/domain_blocks', data).then(() => { const accounts = selectAccountsByDomain(getState(), domain); if (!accounts) return; dispatch(unblockDomainSuccess(domain, accounts)); @@ -99,9 +96,10 @@ const fetchDomainBlocks = () => dispatch(fetchDomainBlocksRequest()); - api(getState).get('/api/v1/domain_blocks').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); + api(getState).get('/api/v1/domain_blocks').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchDomainBlocksSuccess(data, next)); }).catch(err => { dispatch(fetchDomainBlocksFail(err)); }); @@ -134,9 +132,10 @@ const expandDomainBlocks = () => dispatch(expandDomainBlocksRequest()); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandDomainBlocksSuccess(data, next)); }).catch(err => { dispatch(expandDomainBlocksFail(err)); }); diff --git a/src/actions/emoji-reacts.ts b/src/actions/emoji-reacts.ts index 0ebe21c8e..91f726f89 100644 --- a/src/actions/emoji-reacts.ts +++ b/src/actions/emoji-reacts.ts @@ -59,11 +59,11 @@ const fetchEmojiReacts = (id: string, emoji: string) => ? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` : `/api/v1/pleroma/statuses/${id}/reactions`; - return api(getState).get(url).then(response => { - response.data.forEach((emojiReact: APIEntity) => { + return api(getState).get(url).then((response) => response.json()).then((data) => { + data.forEach((emojiReact: APIEntity) => { dispatch(importFetchedAccounts(emojiReact.accounts)); }); - dispatch(fetchEmojiReactsSuccess(id, response.data)); + dispatch(fetchEmojiReactsSuccess(id, data)); }).catch(error => { dispatch(fetchEmojiReactsFail(id, error)); }); @@ -77,10 +77,10 @@ const emojiReact = (status: Status, emoji: string, custom?: string) => return api(getState) .put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`) - .then(function(response) { - dispatch(importFetchedStatus(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); dispatch(emojiReactSuccess(status, emoji)); - }).catch(function(error) { + }).catch((error) => { dispatch(emojiReactFail(status, emoji, error)); }); }; @@ -93,8 +93,8 @@ const unEmojiReact = (status: Status, emoji: string) => return api(getState) .delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`) - .then(response => { - dispatch(importFetchedStatus(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); dispatch(unEmojiReactSuccess(status, emoji)); }).catch(error => { dispatch(unEmojiReactFail(status, emoji, error)); diff --git a/src/actions/events.ts b/src/actions/events.ts index 9026ac710..0e147c1c0 100644 --- a/src/actions/events.ts +++ b/src/actions/events.ts @@ -1,6 +1,6 @@ import { defineMessages, IntlShape } from 'react-intl'; -import api, { getLinks } from 'soapbox/api/index.ts'; +import api from 'soapbox/api/index.ts'; import toast from 'soapbox/toast.tsx'; import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer/index.ts'; @@ -97,7 +97,7 @@ const messages = defineMessages({ const locationSearch = (query: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: LOCATION_SEARCH_REQUEST, query }); - return api(getState).get('/api/v1/pleroma/search/location', { params: { q: query }, signal }).then(({ data: locations }) => { + return api(getState).get('/api/v1/pleroma/search/location', { searchParams: { q: query }, signal }).then((response) => response.json()).then((locations) => { dispatch({ type: LOCATION_SEARCH_SUCCESS, locations }); return locations; }).catch(error => { @@ -161,7 +161,7 @@ const uploadEventBanner = (file: File, intl: IntlShape) => intl, (data) => dispatch(uploadEventBannerSuccess(data, file)), (error) => dispatch(uploadEventBannerFail(error)), - ({ loaded }: any) => { + ({ loaded }: ProgressEvent) => { progress = loaded; dispatch(uploadEventBannerProgress(progress)); }, @@ -223,11 +223,10 @@ const submitEvent = () => if (banner) params.banner_id = banner.id; if (location) params.location_id = location.origin_id; - return api(getState).request({ - url: id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`, - method: id === null ? 'post' : 'put', - data: params, - }).then(({ data }) => { + const method = id === null ? 'POST' : 'PUT'; + const path = id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`; + + return api(getState).request(method, path, params).then((response) => response.json()).then((data) => { dispatch(closeModal('COMPOSE_EVENT')); dispatch(importFetchedStatus(data)); dispatch(submitEventSuccess(data)); @@ -269,7 +268,7 @@ const joinEvent = (id: string, participationMessage?: string) => return api(getState).post(`/api/v1/pleroma/events/${id}/join`, { participation_message: participationMessage, - }).then(({ data }) => { + }).then((response) => response.json()).then((data) => { dispatch(importFetchedStatus(data)); dispatch(joinEventSuccess(data)); toast.success( @@ -311,7 +310,7 @@ const leaveEvent = (id: string) => dispatch(leaveEventRequest(status)); - return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => { + return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then((response) => response.json()).then((data) => { dispatch(importFetchedStatus(data)); dispatch(leaveEventSuccess(data)); }).catch(function(error) { @@ -339,10 +338,11 @@ const fetchEventParticipations = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchEventParticipationsRequest(id)); - return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - return dispatch(fetchEventParticipationsSuccess(id, response.data, next ? next.uri : null)); + return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + return dispatch(fetchEventParticipationsSuccess(id, data, next)); }).catch(error => { dispatch(fetchEventParticipationsFail(id, error)); }); @@ -376,10 +376,11 @@ const expandEventParticipations = (id: string) => dispatch(expandEventParticipationsRequest(id)); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - return dispatch(expandEventParticipationsSuccess(id, response.data, next ? next.uri : null)); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + return dispatch(expandEventParticipationsSuccess(id, data, next)); }).catch(error => { dispatch(expandEventParticipationsFail(id, error)); }); @@ -407,10 +408,11 @@ const fetchEventParticipationRequests = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchEventParticipationRequestsRequest(id)); - return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account))); - return dispatch(fetchEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null)); + return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account))); + return dispatch(fetchEventParticipationRequestsSuccess(id, data, next)); }).catch(error => { dispatch(fetchEventParticipationRequestsFail(id, error)); }); @@ -444,10 +446,11 @@ const expandEventParticipationRequests = (id: string) => dispatch(expandEventParticipationRequestsRequest(id)); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account))); - return dispatch(expandEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null)); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account))); + return dispatch(expandEventParticipationRequestsSuccess(id, data, next)); }).catch(error => { dispatch(expandEventParticipationRequestsFail(id, error)); }); @@ -555,13 +558,13 @@ const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootSt dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); - api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); dispatch({ type: EVENT_FORM_SET, status, - text: response.data.text, - location: response.data.location, + text: data.text, + location: data.location, }); dispatch(openModal('COMPOSE_EVENT')); }).catch(error => { @@ -577,13 +580,15 @@ const fetchRecentEvents = () => dispatch({ type: RECENT_EVENTS_FETCH_REQUEST }); - api(getState).get('/api/v1/timelines/public?only_events=true').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); + api(getState).get('/api/v1/timelines/public?only_events=true').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); dispatch({ type: RECENT_EVENTS_FETCH_SUCCESS, - statuses: response.data, - next: next ? next.uri : null, + statuses: data, + next, }); }).catch(error => { dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error }); @@ -598,13 +603,15 @@ const fetchJoinedEvents = () => dispatch({ type: JOINED_EVENTS_FETCH_REQUEST }); - api(getState).get('/api/v1/pleroma/events/joined_events').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); + api(getState).get('/api/v1/pleroma/events/joined_events').then(async (response) => { + const next = response.next(); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); dispatch({ type: JOINED_EVENTS_FETCH_SUCCESS, - statuses: response.data, - next: next ? next.uri : null, + statuses: data, + next, }); }).catch(error => { dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error }); diff --git a/src/actions/export-data.ts b/src/actions/export-data.ts index 45c1065e4..b219c3fff 100644 --- a/src/actions/export-data.ts +++ b/src/actions/export-data.ts @@ -1,10 +1,10 @@ import { defineMessages } from 'react-intl'; -import api, { getLinks } from 'soapbox/api/index.ts'; +import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts'; +import api from 'soapbox/api/index.ts'; import { normalizeAccount } from 'soapbox/normalizers/index.ts'; import toast from 'soapbox/toast.tsx'; -import type { AxiosResponse } from 'axios'; import type { RootState } from 'soapbox/store.ts'; export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST'; @@ -49,18 +49,31 @@ function fileExport(content: string, fileName: string) { document.body.removeChild(fileToDownload); } -const listAccounts = (getState: () => RootState) => async(apiResponse: AxiosResponse) => { - const followings = apiResponse.data; - let accounts = []; - let next = getLinks(apiResponse).refs.find(link => link.rel === 'next'); - while (next) { - apiResponse = await api(getState).get(next.uri); - next = getLinks(apiResponse).refs.find(link => link.rel === 'next'); - Array.prototype.push.apply(followings, apiResponse.data); - } +const listAccounts = (getState: () => RootState) => { + return async(response: MastodonResponse) => { + let { next } = response.pagination(); + const data = await response.json(); - accounts = followings.map((account: any) => normalizeAccount(account).fqn); - return Array.from(new Set(accounts)); + const map = new Map>(); + + for (const account of data) { + map.set(account.id, account); + } + + while (next) { + const response = await api(getState).get(next); + next = response.pagination().next; + const data = await response.json(); + + for (const account of data) { + map.set(account.id, account); + } + } + + const accts = [...map.values()].map((account) => normalizeAccount(account).fqn); + + return accts; + }; }; export const exportFollows = () => (dispatch: React.Dispatch, getState: () => RootState) => { diff --git a/src/actions/external-auth.ts b/src/actions/external-auth.ts index 414c67886..ba2bc1da0 100644 --- a/src/actions/external-auth.ts +++ b/src/actions/external-auth.ts @@ -21,7 +21,7 @@ import type { AppDispatch, RootState } from 'soapbox/store.ts'; const fetchExternalInstance = (baseURL?: string) => { return baseClient(null, baseURL) .get('/api/v1/instance') - .then(({ data: instance }) => instanceV1Schema.parse(instance)) + .then((response) => response.json()).then((instance) => instanceV1Schema.parse(instance)) .catch(error => { if (error.response?.status === 401) { // Authenticated fetch is enabled. diff --git a/src/actions/familiar-followers.ts b/src/actions/familiar-followers.ts index c1a838ad6..1f52bea70 100644 --- a/src/actions/familiar-followers.ts +++ b/src/actions/familiar-followers.ts @@ -18,7 +18,7 @@ export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: A }); api(getState).get(`/api/v1/accounts/familiar_followers?id[]=${accountId}`) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts; dispatch(importFetchedAccounts(accounts)); diff --git a/src/actions/favourites.ts b/src/actions/favourites.ts index d4240064c..286f57a0f 100644 --- a/src/actions/favourites.ts +++ b/src/actions/favourites.ts @@ -1,6 +1,6 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedStatuses } from './importer/index.ts'; @@ -33,10 +33,11 @@ const fetchFavouritedStatuses = () => dispatch(fetchFavouritedStatusesRequest()); - api(getState).get('/api/v1/favourites').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + api(getState).get('/api/v1/favourites').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(fetchFavouritedStatusesSuccess(data, next)); }).catch(error => { dispatch(fetchFavouritedStatusesFail(error)); }); @@ -72,10 +73,11 @@ const expandFavouritedStatuses = () => dispatch(expandFavouritedStatusesRequest()); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(expandFavouritedStatusesSuccess(data, next)); }).catch(error => { dispatch(expandFavouritedStatusesFail(error)); }); @@ -106,10 +108,11 @@ const fetchAccountFavouritedStatuses = (accountId: string) => dispatch(fetchAccountFavouritedStatusesRequest(accountId)); - api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null)); + api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(fetchAccountFavouritedStatusesSuccess(accountId, data, next)); }).catch(error => { dispatch(fetchAccountFavouritedStatusesFail(accountId, error)); }); @@ -148,10 +151,11 @@ const expandAccountFavouritedStatuses = (accountId: string) => dispatch(expandAccountFavouritedStatusesRequest(accountId)); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); - dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null)); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); + dispatch(expandAccountFavouritedStatusesSuccess(accountId, data, next)); }).catch(error => { dispatch(expandAccountFavouritedStatusesFail(accountId, error)); }); diff --git a/src/actions/filters.ts b/src/actions/filters.ts index cec439816..9bd656a96 100644 --- a/src/actions/filters.ts +++ b/src/actions/filters.ts @@ -44,7 +44,7 @@ const fetchFiltersV1 = () => return api(getState) .get('/api/v1/filters') - .then(({ data }) => dispatch({ + .then((response) => response.json()).then((data) => dispatch({ type: FILTERS_FETCH_SUCCESS, filters: data, skipLoading: true, @@ -66,7 +66,7 @@ const fetchFiltersV2 = () => return api(getState) .get('/api/v2/filters') - .then(({ data }) => dispatch({ + .then((response) => response.json()).then((data) => dispatch({ type: FILTERS_FETCH_SUCCESS, filters: data, skipLoading: true, @@ -101,7 +101,7 @@ const fetchFilterV1 = (id: string) => return api(getState) .get(`/api/v1/filters/${id}`) - .then(({ data }) => dispatch({ + .then((response) => response.json()).then((data) => dispatch({ type: FILTER_FETCH_SUCCESS, filter: data, skipLoading: true, @@ -123,7 +123,7 @@ const fetchFilterV2 = (id: string) => return api(getState) .get(`/api/v2/filters/${id}`) - .then(({ data }) => dispatch({ + .then((response) => response.json()).then((data) => dispatch({ type: FILTER_FETCH_SUCCESS, filter: data, skipLoading: true, @@ -156,8 +156,8 @@ const createFilterV1 = (title: string, expires_in: string | null, context: Array irreversible: hide, whole_word: keywords[0].whole_word, expires_in, - }).then(response => { - dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data }); + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data }); toast.success(messages.added); }).catch(error => { dispatch({ type: FILTERS_CREATE_FAIL, error }); @@ -173,8 +173,8 @@ const createFilterV2 = (title: string, expires_in: string | null, context: Array filter_action: hide ? 'hide' : 'warn', expires_in, keywords_attributes, - }).then(response => { - dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data }); + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data }); toast.success(messages.added); }).catch(error => { dispatch({ type: FILTERS_CREATE_FAIL, error }); @@ -201,8 +201,8 @@ const updateFilterV1 = (id: string, title: string, expires_in: string | null, co irreversible: hide, whole_word: keywords[0].whole_word, expires_in, - }).then(response => { - dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data }); + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data }); toast.success(messages.added); }).catch(error => { dispatch({ type: FILTERS_UPDATE_FAIL, error }); @@ -218,8 +218,8 @@ const updateFilterV2 = (id: string, title: string, expires_in: string | null, co filter_action: hide ? 'hide' : 'warn', expires_in, keywords_attributes, - }).then(response => { - dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data }); + }).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data }); toast.success(messages.added); }).catch(error => { dispatch({ type: FILTERS_UPDATE_FAIL, error }); @@ -240,8 +240,8 @@ const updateFilter = (id: string, title: string, expires_in: string | null, cont const deleteFilterV1 = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: FILTERS_DELETE_REQUEST }); - return api(getState).delete(`/api/v1/filters/${id}`).then(response => { - dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data }); + return api(getState).delete(`/api/v1/filters/${id}`).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data }); toast.success(messages.removed); }).catch(error => { dispatch({ type: FILTERS_DELETE_FAIL, error }); @@ -251,8 +251,8 @@ const deleteFilterV1 = (id: string) => const deleteFilterV2 = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: FILTERS_DELETE_REQUEST }); - return api(getState).delete(`/api/v2/filters/${id}`).then(response => { - dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data }); + return api(getState).delete(`/api/v2/filters/${id}`).then((response) => response.json()).then((data) => { + dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data }); toast.success(messages.removed); }).catch(error => { dispatch({ type: FILTERS_DELETE_FAIL, error }); diff --git a/src/actions/groups.ts b/src/actions/groups.ts index 0b11d8c58..ac96ba413 100644 --- a/src/actions/groups.ts +++ b/src/actions/groups.ts @@ -1,6 +1,6 @@ import { deleteEntities } from 'soapbox/entity-store/actions.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchRelationships } from './accounts.ts'; import { importFetchedGroups, importFetchedAccounts } from './importer/index.ts'; @@ -114,7 +114,7 @@ const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootS dispatch(fetchGroupRequest(id)); return api(getState).get(`/api/v1/groups/${id}`) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch(importFetchedGroups([data])); dispatch(fetchGroupSuccess(data)); }) @@ -141,7 +141,7 @@ const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => dispatch(fetchGroupsRequest()); return api(getState).get('/api/v1/groups') - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch(importFetchedGroups(data)); dispatch(fetchGroupsSuccess(data)); dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id))); @@ -174,8 +174,8 @@ const fetchGroupRelationships = (groupIds: string[]) => dispatch(fetchGroupRelationshipsRequest(newGroupIds)); - return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchGroupRelationshipsSuccess(response.data)); + return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then((response) => response.json()).then((data) => { + dispatch(fetchGroupRelationshipsSuccess(data)); }).catch(error => { dispatch(fetchGroupRelationshipsFail(error)); }); @@ -232,11 +232,12 @@ const fetchGroupBlocks = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupBlocksRequest(id)); - return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(`/api/v1/groups/${id}/blocks`).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchGroupBlocksSuccess(id, response.data, next ? next.uri : null)); + dispatch(importFetchedAccounts(data)); + dispatch(fetchGroupBlocksSuccess(id, data, next)); }).catch(error => { dispatch(fetchGroupBlocksFail(id, error)); }); @@ -271,12 +272,13 @@ const expandGroupBlocks = (id: string) => dispatch(expandGroupBlocksRequest(id)); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandGroupBlocksSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(expandGroupBlocksSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandGroupBlocksFail(id, error)); }); @@ -361,7 +363,8 @@ const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole dispatch(groupPromoteAccountRequest(groupId, accountId)); return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role }) - .then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data))) + .then((response) => response.json()) + .then((data) => dispatch(groupPromoteAccountSuccess(groupId, accountId, data))) .catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err))); }; @@ -390,7 +393,8 @@ const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole) dispatch(groupDemoteAccountRequest(groupId, accountId)); return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role }) - .then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data))) + .then((response) => response.json()) + .then((data) => dispatch(groupDemoteAccountSuccess(groupId, accountId, data))) .catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err))); }; @@ -418,11 +422,12 @@ const fetchGroupMemberships = (id: string, role: GroupRole) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupMembershipsRequest(id, role)); - return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(`/api/v1/groups/${id}/memberships`, { searchParams: { role } }).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account))); - dispatch(fetchGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null)); + dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account))); + dispatch(fetchGroupMembershipsSuccess(id, role, data, next)); }).catch(error => { dispatch(fetchGroupMembershipsFail(id, role, error)); }); @@ -460,12 +465,13 @@ const expandGroupMemberships = (id: string, role: GroupRole) => dispatch(expandGroupMembershipsRequest(id, role)); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account))); - dispatch(expandGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account))); + dispatch(expandGroupMembershipsSuccess(id, role, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandGroupMembershipsFail(id, role, error)); }); @@ -496,11 +502,12 @@ const fetchGroupMembershipRequests = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchGroupMembershipRequestsRequest(id)); - return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null)); + dispatch(importFetchedAccounts(data)); + dispatch(fetchGroupMembershipRequestsSuccess(id, data, next)); }).catch(error => { dispatch(fetchGroupMembershipRequestsFail(id, error)); }); @@ -535,12 +542,13 @@ const expandGroupMembershipRequests = (id: string) => dispatch(expandGroupMembershipRequestsRequest(id)); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); - dispatch(importFetchedAccounts(response.data)); - dispatch(expandGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); + dispatch(importFetchedAccounts(data)); + dispatch(expandGroupMembershipRequestsSuccess(id, data, next)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandGroupMembershipRequestsFail(id, error)); }); diff --git a/src/actions/history.ts b/src/actions/history.ts index 95f71a929..7c05c77f5 100644 --- a/src/actions/history.ts +++ b/src/actions/history.ts @@ -19,7 +19,7 @@ const fetchHistory = (statusId: string) => dispatch(fetchHistoryRequest(statusId)); - api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => { + api(getState).get(`/api/v1/statuses/${statusId}/history`).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data.map((x: APIEntity) => x.account))); dispatch(fetchHistorySuccess(statusId, data)); }).catch(error => dispatch(fetchHistoryFail(error))); diff --git a/src/actions/import-data.ts b/src/actions/import-data.ts index 0aa5ebaa4..e0bec2464 100644 --- a/src/actions/import-data.ts +++ b/src/actions/import-data.ts @@ -43,9 +43,9 @@ export const importFollows = (params: FormData) => dispatch({ type: IMPORT_FOLLOWS_REQUEST }); return api(getState) .post('/api/pleroma/follow_import', params) - .then(response => { + .then((response) => response.json()).then((data) => { toast.success(messages.followersSuccess); - dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data }); + dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: data }); }).catch(error => { dispatch({ type: IMPORT_FOLLOWS_FAIL, error }); }); @@ -56,9 +56,9 @@ export const importBlocks = (params: FormData) => dispatch({ type: IMPORT_BLOCKS_REQUEST }); return api(getState) .post('/api/pleroma/blocks_import', params) - .then(response => { + .then((response) => response.json()).then((data) => { toast.success(messages.blocksSuccess); - dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data }); + dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: data }); }).catch(error => { dispatch({ type: IMPORT_BLOCKS_FAIL, error }); }); @@ -69,9 +69,9 @@ export const importMutes = (params: FormData) => dispatch({ type: IMPORT_MUTES_REQUEST }); return api(getState) .post('/api/pleroma/mutes_import', params) - .then(response => { + .then((response) => response.json()).then((data) => { toast.success(messages.mutesSuccess); - dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data }); + dispatch({ type: IMPORT_MUTES_SUCCESS, config: data }); }).catch(error => { dispatch({ type: IMPORT_MUTES_FAIL, error }); }); diff --git a/src/actions/instance.ts b/src/actions/instance.ts index 9adc7bc1d..371c5426d 100644 --- a/src/actions/instance.ts +++ b/src/actions/instance.ts @@ -27,7 +27,8 @@ export const fetchInstance = createAsyncThunk { try { - const { data } = await api(getState).get('/api/v1/instance'); + const response = await api(getState).get('/api/v1/instance'); + const data = await response.json(); const instance = instanceV1Schema.parse(data); const features = getFeatures(instance); @@ -46,7 +47,8 @@ export const fetchInstanceV2 = createAsyncThunk { try { - const { data } = await api(getState).get('/api/v2/instance'); + const response = await api(getState).get('/api/v2/instance'); + const data = await response.json(); const instance = instanceV2Schema.parse(data); return { instance, host }; } catch (e) { diff --git a/src/actions/interactions.ts b/src/actions/interactions.ts index 4c06276d7..eae80d39d 100644 --- a/src/actions/interactions.ts +++ b/src/actions/interactions.ts @@ -3,7 +3,7 @@ import { defineMessages } from 'react-intl'; import toast from 'soapbox/toast.tsx'; import { isLoggedIn } from 'soapbox/utils/auth.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchRelationships } from './accounts.ts'; import { importFetchedAccounts, importFetchedStatus } from './importer/index.ts'; @@ -101,10 +101,10 @@ const reblog = (status: StatusEntity) => dispatch(reblogRequest(status)); - api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then(function(response) { + api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then((response) => response.json()).then((data) => { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper - dispatch(importFetchedStatus(response.data.reblog)); + dispatch(importFetchedStatus(data.reblog)); dispatch(reblogSuccess(status)); }).catch(error => { dispatch(reblogFail(status, error)); @@ -322,8 +322,8 @@ const zap = (account: AccountEntity, status: StatusEntity | undefined, amount: n if (status) dispatch(zapRequest(status)); - return api(getState).post('/api/v1/ditto/zap', { amount, comment, account_id: account.id, status_id: status?.id }).then(async function(response) { - const { invoice } = response.data; + return api(getState).post('/api/v1/ditto/zap', { amount, comment, account_id: account.id, status_id: status?.id }).then(async (response) => { + const { invoice } = await response.json(); if (!invoice) throw Error('Could not generate invoice'); if (!window.webln) return invoice; @@ -363,9 +363,9 @@ const bookmark = (status: StatusEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(bookmarkRequest(status)); - return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then(function(response) { - dispatch(importFetchedStatus(response.data)); - dispatch(bookmarkSuccess(status, response.data)); + return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(bookmarkSuccess(status, data)); toast.success(messages.bookmarkAdded, { actionLink: '/bookmarks', @@ -379,9 +379,9 @@ const unbookmark = (status: StatusEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(unbookmarkRequest(status)); - api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then(response => { - dispatch(importFetchedStatus(response.data)); - dispatch(unbookmarkSuccess(status, response.data)); + api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); + dispatch(unbookmarkSuccess(status, data)); toast.success(messages.bookmarkRemoved); }).catch(error => { dispatch(unbookmarkFail(status, error)); @@ -437,11 +437,12 @@ const fetchReblogs = (id: string) => dispatch(fetchReblogsRequest(id)); - api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchReblogsSuccess(id, data, next)); }).catch(error => { dispatch(fetchReblogsFail(id, error)); }); @@ -467,11 +468,12 @@ const fetchReblogsFail = (id: string, error: unknown) => ({ const expandReblogs = (id: string, path: string) => (dispatch: AppDispatch, getState: () => RootState) => { - api(getState).get(path).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(expandReblogsSuccess(id, data, next)); }).catch(error => { dispatch(expandReblogsFail(id, error)); }); @@ -496,11 +498,12 @@ const fetchFavourites = (id: string) => dispatch(fetchFavouritesRequest(id)); - api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchFavouritesSuccess(id, data, next)); }).catch(error => { dispatch(fetchFavouritesFail(id, error)); }); @@ -526,11 +529,12 @@ const fetchFavouritesFail = (id: string, error: unknown) => ({ const expandFavourites = (id: string, path: string) => (dispatch: AppDispatch, getState: () => RootState) => { - api(getState).get(path).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(expandFavouritesSuccess(id, data, next)); }).catch(error => { dispatch(expandFavouritesFail(id, error)); }); @@ -555,10 +559,10 @@ const fetchDislikes = (id: string) => dispatch(fetchDislikesRequest(id)); - api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => { - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - dispatch(fetchDislikesSuccess(id, response.data)); + api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.id))); + dispatch(fetchDislikesSuccess(id, data)); }).catch(error => { dispatch(fetchDislikesFail(id, error)); }); @@ -585,9 +589,9 @@ const fetchReactions = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchReactionsRequest(id)); - api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => { - dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ accounts }) => accounts).flat())); - dispatch(fetchReactionsSuccess(id, response.data)); + api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccounts((data as APIEntity[]).map(({ accounts }) => accounts).flat())); + dispatch(fetchReactionsSuccess(id, data)); }).catch(error => { dispatch(fetchReactionsFail(id, error)); }); @@ -614,10 +618,11 @@ const fetchZaps = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchZapsRequest(id)); - api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ account }) => account).flat())); - dispatch(fetchZapsSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts((data as APIEntity[]).map(({ account }) => account).flat())); + dispatch(fetchZapsSuccess(id, data, next)); }).catch(error => { dispatch(fetchZapsFail(id, error)); }); @@ -643,11 +648,12 @@ const fetchZapsFail = (id: string, error: unknown) => ({ const expandZaps = (id: string, path: string) => (dispatch: AppDispatch, getState: () => RootState) => { - api(getState).get(path).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map((item: APIEntity) => item.account))); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.account.id))); - dispatch(expandZapsSuccess(id, response.data, next ? next.uri : null)); + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedAccounts(data.map((item: APIEntity) => item.account))); + dispatch(fetchRelationships(data.map((item: APIEntity) => item.account.id))); + dispatch(expandZapsSuccess(id, data, next)); }).catch(error => { dispatch(expandZapsFail(id, error)); }); @@ -672,8 +678,8 @@ const pin = (status: StatusEntity) => dispatch(pinRequest(status)); - api(getState).post(`/api/v1/statuses/${status.id}/pin`).then(response => { - dispatch(importFetchedStatus(response.data)); + api(getState).post(`/api/v1/statuses/${status.id}/pin`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); dispatch(pinSuccess(status)); }).catch(error => { dispatch(pinFail(status, error)); @@ -719,8 +725,8 @@ const unpin = (status: StatusEntity) => dispatch(unpinRequest(status)); - api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then(response => { - dispatch(importFetchedStatus(response.data)); + api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatus(data)); dispatch(unpinSuccess(status)); }).catch(error => { dispatch(unpinFail(status, error)); @@ -759,7 +765,7 @@ const remoteInteraction = (ap_id: string, profile: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(remoteInteractionRequest(ap_id, profile)); - return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => { + return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then((response) => response.json()).then((data) => { if (data.error) throw new Error(data.error); dispatch(remoteInteractionSuccess(ap_id, profile, data.url)); diff --git a/src/actions/lists.ts b/src/actions/lists.ts index 0df09c068..aa7cc0966 100644 --- a/src/actions/lists.ts +++ b/src/actions/lists.ts @@ -66,7 +66,7 @@ const fetchList = (id: string | number) => (dispatch: AppDispatch, getState: () dispatch(fetchListRequest(id)); api(getState).get(`/api/v1/lists/${id}`) - .then(({ data }) => dispatch(fetchListSuccess(data))) + .then((response) => response.json()).then((data) => dispatch(fetchListSuccess(data))) .catch(err => dispatch(fetchListFail(id, err))); }; @@ -92,7 +92,7 @@ const fetchLists = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchListsRequest()); api(getState).get('/api/v1/lists') - .then(({ data }) => dispatch(fetchListsSuccess(data))) + .then((response) => response.json()).then((data) => dispatch(fetchListsSuccess(data))) .catch(err => dispatch(fetchListsFail(err))); }; @@ -140,7 +140,7 @@ const createList = (title: string, shouldReset?: boolean) => (dispatch: AppDispa dispatch(createListRequest()); - api(getState).post('/api/v1/lists', { title }).then(({ data }) => { + api(getState).post('/api/v1/lists', { title }).then((response) => response.json()).then((data) => { dispatch(createListSuccess(data)); if (shouldReset) { @@ -168,7 +168,7 @@ const updateList = (id: string | number, title: string, shouldReset?: boolean) = dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title }).then((response) => response.json()).then((data) => { dispatch(updateListSuccess(data)); if (shouldReset) { @@ -228,7 +228,7 @@ const fetchListAccounts = (listId: string | number) => (dispatch: AppDispatch, g dispatch(fetchListAccountsRequest(listId)); - api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + api(getState).get(`/api/v1/lists/${listId}/accounts`, { searchParams: { limit: 0 } }).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data)); dispatch(fetchListAccountsSuccess(listId, data, null)); }).catch(err => dispatch(fetchListAccountsFail(listId, err))); @@ -255,14 +255,14 @@ const fetchListAccountsFail = (id: string | number, error: unknown) => ({ const fetchListSuggestions = (q: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; - const params = { + const searchParams = { q, resolve: false, limit: 4, following: true, }; - api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + api(getState).get('/api/v1/accounts/search', { searchParams }).then((response) => response.json()).then((data) => { dispatch(importFetchedAccounts(data)); dispatch(fetchListSuggestionsReady(q, data)); }).catch(error => toast.showAlertForError(error)); @@ -325,7 +325,10 @@ const removeFromList = (listId: string | number, accountId: string) => (dispatch dispatch(removeFromListRequest(listId, accountId)); - api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } }) + const data = new FormData(); + data.append('account_ids[]', accountId); + + api(getState).request('DELETE', `/api/v1/lists/${listId}/accounts`, data) .then(() => dispatch(removeFromListSuccess(listId, accountId))) .catch(err => dispatch(removeFromListFail(listId, accountId, err))); }; @@ -368,7 +371,7 @@ const fetchAccountLists = (accountId: string) => (dispatch: AppDispatch, getStat dispatch(fetchAccountListsRequest(accountId)); api(getState).get(`/api/v1/accounts/${accountId}/lists`) - .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data))) + .then((response) => response.json()).then((data) => dispatch(fetchAccountListsSuccess(accountId, data))) .catch(err => dispatch(fetchAccountListsFail(accountId, err))); }; diff --git a/src/actions/markers.ts b/src/actions/markers.ts index 33dc1f6c4..ad0f2a3c7 100644 --- a/src/actions/markers.ts +++ b/src/actions/markers.ts @@ -14,9 +14,7 @@ const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL'; const fetchMarker = (timeline: Array) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MARKER_FETCH_REQUEST }); - return api(getState).get('/api/v1/markers', { - params: { timeline }, - }).then(({ data: marker }) => { + return api(getState).get('/api/v1/markers', { searchParams: { timeline } }).then((response) => response.json()).then((marker) => { dispatch({ type: MARKER_FETCH_SUCCESS, marker }); }).catch(error => { dispatch({ type: MARKER_FETCH_FAIL, error }); @@ -26,7 +24,7 @@ const fetchMarker = (timeline: Array) => const saveMarker = (marker: APIEntity) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MARKER_SAVE_REQUEST, marker }); - return api(getState).post('/api/v1/markers', marker).then(({ data: marker }) => { + return api(getState).post('/api/v1/markers', marker).then((response) => response.json()).then((marker) => { dispatch({ type: MARKER_SAVE_SUCCESS, marker }); }).catch(error => { dispatch({ type: MARKER_SAVE_FAIL, error }); diff --git a/src/actions/me.ts b/src/actions/me.ts index 01354a1df..a524daccc 100644 --- a/src/actions/me.ts +++ b/src/actions/me.ts @@ -7,7 +7,6 @@ import api from '../api/index.ts'; import { verifyCredentials } from './auth.ts'; import { importFetchedAccount } from './importer/index.ts'; -import type { RawAxiosRequestHeaders } from 'axios'; import type { Account } from 'soapbox/schemas/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity } from 'soapbox/types/entities.ts'; @@ -58,14 +57,14 @@ const patchMe = (params: Record, isFormData = false) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(patchMeRequest()); - const headers: RawAxiosRequestHeaders = isFormData ? { + const headers = isFormData ? { 'Content-Type': 'multipart/form-data', - } : {}; + } : undefined; return api(getState) .patch('/api/v1/accounts/update_credentials', params, { headers }) - .then(response => { - dispatch(patchMeSuccess(response.data)); + .then((response) => response.json()).then((data) => { + dispatch(patchMeSuccess(data)); }).catch(error => { dispatch(patchMeFail(error)); throw error; diff --git a/src/actions/media.ts b/src/actions/media.ts index f009854c4..c19895a3b 100644 --- a/src/actions/media.ts +++ b/src/actions/media.ts @@ -31,15 +31,11 @@ const updateMedia = (mediaId: string, params: Record) => const uploadMediaV1 = (data: FormData, onUploadProgress = noOp) => (dispatch: any, getState: () => RootState) => - api(getState).post('/api/v1/media', data, { - onUploadProgress: onUploadProgress, - }); + api(getState).post('/api/v1/media', data, { onUploadProgress }); const uploadMediaV2 = (data: FormData, onUploadProgress = noOp) => (dispatch: any, getState: () => RootState) => - api(getState).post('/api/v2/media', data, { - onUploadProgress: onUploadProgress, - }); + api(getState).post('/api/v2/media', data, { onUploadProgress }); const uploadMedia = (data: FormData, onUploadProgress = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { @@ -59,8 +55,7 @@ const uploadFile = ( intl: IntlShape, onSuccess: (data: APIEntity) => void = () => {}, onFail: (error: unknown) => void = () => {}, - onProgress: (loaded: number) => void = () => {}, - changeTotal: (value: number) => void = () => {}, + onUploadProgress: (e: ProgressEvent) => void = () => {}, ) => async (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return; @@ -92,21 +87,23 @@ const uploadFile = ( } // FIXME: Don't define const in loop - resizeImage(file).then(resized => { + resizeImage(file).then((resized) => { const data = new FormData(); data.append('file', resized); - // Account for disparity in size of original image and resized data - changeTotal(resized.size - file.size); - return dispatch(uploadMedia(data, onProgress)) - .then(({ status, data }) => { + return dispatch(uploadMedia(data, onUploadProgress)) + .then(async (response) => { + const { status } = response; + const data = await response.json(); // If server-side processing of the media attachment has not completed yet, // poll the server until it is, before showing the media attachment as uploaded if (status === 200) { onSuccess(data); } else if (status === 202) { const poll = () => { - dispatch(fetchMedia(data.id)).then(({ status, data }) => { + dispatch(fetchMedia(data.id)).then(async (response) => { + const { status } = response; + const data = await response.json(); if (status === 200) { onSuccess(data); } else if (status === 206) { diff --git a/src/actions/mfa.ts b/src/actions/mfa.ts index 2320e4d37..6d62f133a 100644 --- a/src/actions/mfa.ts +++ b/src/actions/mfa.ts @@ -25,7 +25,7 @@ const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL'; const fetchMfa = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MFA_FETCH_REQUEST }); - return api(getState).get('/api/pleroma/accounts/mfa').then(({ data }) => { + return api(getState).get('/api/pleroma/accounts/mfa').then((response) => response.json()).then((data) => { dispatch({ type: MFA_FETCH_SUCCESS, data }); }).catch(() => { dispatch({ type: MFA_FETCH_FAIL }); @@ -35,7 +35,7 @@ const fetchMfa = () => const fetchBackupCodes = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST }); - return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(({ data }) => { + return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then((response) => response.json()).then((data) => { dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data }); return data; }).catch(() => { @@ -46,7 +46,7 @@ const fetchBackupCodes = () => const setupMfa = (method: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MFA_SETUP_REQUEST, method }); - return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then(({ data }) => { + return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then((response) => response.json()).then((data) => { dispatch({ type: MFA_SETUP_SUCCESS, data }); return data; }).catch((error: unknown) => { @@ -59,7 +59,7 @@ const confirmMfa = (method: string, code: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => { const params = { code, password }; dispatch({ type: MFA_CONFIRM_REQUEST, method, code }); - return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => { + return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then((response) => response.json()).then((data) => { dispatch({ type: MFA_CONFIRM_SUCCESS, method, code }); return data; }).catch((error: unknown) => { @@ -71,7 +71,7 @@ const confirmMfa = (method: string, code: string, password: string) => const disableMfa = (method: string, password: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: MFA_DISABLE_REQUEST, method }); - return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => { + return api(getState).request('DELETE', `/api/pleroma/accounts/mfa/${method}`, { password }).then((response) => response.json()).then((data) => { dispatch({ type: MFA_DISABLE_SUCCESS, method }); return data; }).catch((error: unknown) => { diff --git a/src/actions/notifications.test.ts b/src/actions/notifications.test.ts deleted file mode 100644 index 1f67081dd..000000000 --- a/src/actions/notifications.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { OrderedMap as ImmutableOrderedMap } from 'immutable'; -import { describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; -import { normalizeNotification } from 'soapbox/normalizers/index.ts'; - -import { markReadNotifications } from './notifications.ts'; - -describe('markReadNotifications()', () => { - it('fires off marker when top notification is newer than lastRead', async() => { - __stub((mock) => mock.onPost('/api/v1/markers').reply(200, {})); - - const items = ImmutableOrderedMap({ - '10': normalizeNotification({ id: '10' }), - }); - - const state = { - ...rootState, - me: '123', - notifications: rootState.notifications.merge({ - lastRead: '9', - items, - }), - }; - - const store = mockStore(state); - - const expectedActions = [{ - type: 'MARKER_SAVE_REQUEST', - marker: { - notifications: { - last_read_id: '10', - }, - }, - }]; - - store.dispatch(markReadNotifications()); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); -}); diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index 2c7a9ce38..04d7e5dcb 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -2,7 +2,7 @@ import IntlMessageFormat from 'intl-messageformat'; import 'intl-pluralrules'; import { defineMessages } from 'react-intl'; -import api, { getLinks } from 'soapbox/api/index.ts'; +import api from 'soapbox/api/index.ts'; import { getFilters, regexFromFilters } from 'soapbox/selectors/index.ts'; import { isLoggedIn } from 'soapbox/utils/auth.ts'; import { compareId } from 'soapbox/utils/comparators.ts'; @@ -213,10 +213,11 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an dispatch(expandNotificationsRequest(isLoadingMore)); - return api(getState).get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get('/api/v1/notifications', { searchParams: params }).then(async (response) => { + const next = response.next(); + const data = await response.json(); - const entries = (response.data as APIEntity[]).reduce((acc, item) => { + const entries = (data as APIEntity[]).reduce((acc, item) => { if (item.account?.id) { acc.accounts[item.account.id] = item.account; } @@ -239,8 +240,8 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group); dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); - fetchRelatedRelationships(dispatch, response.data); + dispatch(expandNotificationsSuccess(data, next, isLoadingMore)); + fetchRelatedRelationships(dispatch, data); done(); }).catch(error => { dispatch(expandNotificationsFail(error, isLoadingMore)); diff --git a/src/actions/oauth.ts b/src/actions/oauth.ts index 10b5c1076..2ee114c29 100644 --- a/src/actions/oauth.ts +++ b/src/actions/oauth.ts @@ -23,7 +23,7 @@ export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL'; export const obtainOAuthToken = (params: Record, baseURL?: string) => (dispatch: AppDispatch) => { dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params }); - return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => { + return baseClient(null, baseURL).post('/oauth/token', params).then((response) => response.json()).then((token) => { dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token }); return token; }).catch(error => { @@ -36,7 +36,7 @@ export const revokeOAuthToken = (params: Record) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params }); const baseURL = getBaseURL(getState()); - return baseClient(null, baseURL).post('/oauth/revoke', params).then(({ data }) => { + return baseClient(null, baseURL).post('/oauth/revoke', params).then((response) => response.json()).then((data) => { dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data }); return data; }).catch(error => { diff --git a/src/actions/patron.ts b/src/actions/patron.ts index 4a3d5eade..414d62910 100644 --- a/src/actions/patron.ts +++ b/src/actions/patron.ts @@ -14,8 +14,8 @@ const PATRON_ACCOUNT_FETCH_FAIL = 'PATRON_ACCOUNT_FETCH_FAIL'; const fetchPatronInstance = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: PATRON_INSTANCE_FETCH_REQUEST }); - return api(getState).get('/api/patron/v1/instance').then(response => { - dispatch(importFetchedInstance(response.data)); + return api(getState).get('/api/patron/v1/instance').then((response) => response.json()).then((data) => { + dispatch(importFetchedInstance(data)); }).catch(error => { dispatch(fetchInstanceFail(error)); }); @@ -25,8 +25,8 @@ const fetchPatronAccount = (apId: string) => (dispatch: AppDispatch, getState: () => RootState) => { apId = encodeURIComponent(apId); dispatch({ type: PATRON_ACCOUNT_FETCH_REQUEST }); - api(getState).get(`/api/patron/v1/accounts/${apId}`).then(response => { - dispatch(importFetchedAccount(response.data)); + api(getState).get(`/api/patron/v1/accounts/${apId}`).then((response) => response.json()).then((data) => { + dispatch(importFetchedAccount(data)); }).catch(error => { dispatch(fetchAccountFail(error)); }); diff --git a/src/actions/pin-statuses.ts b/src/actions/pin-statuses.ts index a09a71fa9..850456615 100644 --- a/src/actions/pin-statuses.ts +++ b/src/actions/pin-statuses.ts @@ -18,9 +18,9 @@ const fetchPinnedStatuses = () => dispatch(fetchPinnedStatusesRequest()); - api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { - dispatch(importFetchedStatuses(response.data)); - dispatch(fetchPinnedStatusesSuccess(response.data, null)); + api(getState).get(`/api/v1/accounts/${me}/statuses`, { searchParams: { pinned: true } }).then((response) => response.json()).then((data) => { + dispatch(importFetchedStatuses(data)); + dispatch(fetchPinnedStatusesSuccess(data, null)); }).catch(error => { dispatch(fetchPinnedStatusesFail(error)); }); diff --git a/src/actions/polls.ts b/src/actions/polls.ts index 57a83e08e..4357ed7c6 100644 --- a/src/actions/polls.ts +++ b/src/actions/polls.ts @@ -18,7 +18,7 @@ const vote = (pollId: string, choices: string[]) => dispatch(voteRequest()); api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices }) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch(importFetchedPoll(data)); dispatch(voteSuccess(data)); }) @@ -30,7 +30,7 @@ const fetchPoll = (pollId: string) => dispatch(fetchPollRequest()); api(getState).get(`/api/v1/polls/${pollId}`) - .then(({ data }) => { + .then((response) => response.json()).then((data) => { dispatch(importFetchedPoll(data)); dispatch(fetchPollSuccess(data)); }) diff --git a/src/actions/preload.test.ts b/src/actions/preload.test.ts deleted file mode 100644 index 2ecf454e1..000000000 --- a/src/actions/preload.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore } from 'soapbox/jest/test-helpers.tsx'; - -import { VERIFY_CREDENTIALS_REQUEST } from './auth.ts'; -import { ACCOUNTS_IMPORT } from './importer/index.ts'; -import { - MASTODON_PRELOAD_IMPORT, - preloadMastodon, -} from './preload.ts'; - -describe('preloadMastodon()', () => { - it('creates the expected actions', async () => { - const data = await import('soapbox/__fixtures__/mastodon_initial_state.json'); - - __stub(mock => { - mock.onGet('/api/v1/accounts/verify_credentials') - .reply(200, {}); - }); - - const store = mockStore({}); - store.dispatch(preloadMastodon(data)); - const actions = store.getActions(); - - expect(actions[0].type).toEqual(ACCOUNTS_IMPORT); - expect(actions[0].accounts[0].username).toEqual('Gargron'); - expect(actions[0].accounts[1].username).toEqual('benis911'); - - expect(actions[1]).toEqual({ - type: VERIFY_CREDENTIALS_REQUEST, - token: 'Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q', - }); - - expect(actions[2]).toEqual({ type: MASTODON_PRELOAD_IMPORT, data }); - }); -}); diff --git a/src/actions/scheduled-statuses.ts b/src/actions/scheduled-statuses.ts index 7bd8d0331..c149fe89e 100644 --- a/src/actions/scheduled-statuses.ts +++ b/src/actions/scheduled-statuses.ts @@ -1,6 +1,6 @@ import { getFeatures } from 'soapbox/utils/features.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity } from 'soapbox/types/entities.ts'; @@ -32,9 +32,10 @@ const fetchScheduledStatuses = () => dispatch(fetchScheduledStatusesRequest()); - api(getState).get('/api/v1/scheduled_statuses').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null)); + api(getState).get('/api/v1/scheduled_statuses').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchScheduledStatusesSuccess(data, next)); }).catch(error => { dispatch(fetchScheduledStatusesFail(error)); }); @@ -43,7 +44,7 @@ const fetchScheduledStatuses = () => const cancelScheduledStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id }); - api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(({ data }) => { + api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then((response) => response.json()).then((data) => { dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data }); }).catch(error => { dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error }); @@ -75,9 +76,10 @@ const expandScheduledStatuses = () => dispatch(expandScheduledStatusesRequest()); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null)); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandScheduledStatusesSuccess(data, next)); }).catch(error => { dispatch(expandScheduledStatusesFail(error)); }); diff --git a/src/actions/search.ts b/src/actions/search.ts index 82a96c009..7e84febd3 100644 --- a/src/actions/search.ts +++ b/src/actions/search.ts @@ -1,4 +1,4 @@ -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchRelationships } from './accounts.ts'; import { importFetchedAccounts, importFetchedStatuses } from './importer/index.ts'; @@ -72,20 +72,21 @@ const submitSearch = (filter?: SearchFilter) => if (accountId) params.account_id = accountId; api(getState).get('/api/v2/search', { - params, - }).then(response => { - if (response.data.accounts) { - dispatch(importFetchedAccounts(response.data.accounts)); + searchParams: params, + }).then(async (response) => { + const next = response.next(); + const data = await response.json(); + + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); } - if (response.data.statuses) { - dispatch(importFetchedStatuses(response.data.statuses)); + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); } - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id))); + dispatch(fetchSearchSuccess(data, value, type, next)); + dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); }); @@ -143,9 +144,10 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( } api(getState).get(url, { - params, - }).then(response => { - const data = response.data; + searchParams: params, + }).then(async (response) => { + const next = response.next(); + const data = await response.json(); if (data.accounts) { dispatch(importFetchedAccounts(data.accounts)); @@ -155,9 +157,7 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: ( dispatch(importFetchedStatuses(data.statuses)); } - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null)); + dispatch(expandSearchSuccess(data, value, type, next)); dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id))); }).catch(error => { dispatch(expandSearchFail(error)); diff --git a/src/actions/security.ts b/src/actions/security.ts index 683a3a6e6..c64c14347 100644 --- a/src/actions/security.ts +++ b/src/actions/security.ts @@ -50,7 +50,7 @@ const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL'; const fetchOAuthTokens = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: FETCH_TOKENS_REQUEST }); - return api(getState).get('/api/oauth_tokens').then(({ data: tokens }) => { + return api(getState).get('/api/oauth_tokens').then((response) => response.json()).then((tokens) => { dispatch({ type: FETCH_TOKENS_SUCCESS, tokens }); }).catch(() => { dispatch({ type: FETCH_TOKENS_FAIL }); @@ -74,9 +74,9 @@ const changePassword = (oldPassword: string, newPassword: string, confirmation: password: oldPassword, new_password: newPassword, new_password_confirmation: confirmation, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_PASSWORD_SUCCESS, data }); }).catch(error => { dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); throw error; @@ -128,9 +128,9 @@ const changeEmail = (email: string, password: string) => return api(getState).post('/api/pleroma/change_email', { email, password, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_EMAIL_SUCCESS, email, data }); }).catch(error => { dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); throw error; @@ -148,9 +148,9 @@ const deleteAccount = (password: string) => dispatch({ type: DELETE_ACCOUNT_REQUEST }); return api(getState).post('/api/pleroma/delete_account', { password, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: DELETE_ACCOUNT_SUCCESS, response }); + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: DELETE_ACCOUNT_SUCCESS, data }); dispatch({ type: AUTH_LOGGED_OUT, account }); toast.success(messages.loggedOut); }).catch(error => { @@ -165,9 +165,9 @@ const moveAccount = (targetAccount: string, password: string) => return api(getState).post('/api/pleroma/move_account', { password, target_account: targetAccount, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: MOVE_ACCOUNT_SUCCESS, response }); + }).then((response) => response.json()).then((data) => { + if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: MOVE_ACCOUNT_SUCCESS, data }); }).catch(error => { dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true }); throw error; diff --git a/src/actions/soapbox.ts b/src/actions/soapbox.ts index dd9ad5d25..d53fabbf7 100644 --- a/src/actions/soapbox.ts +++ b/src/actions/soapbox.ts @@ -33,7 +33,7 @@ const fetchFrontendConfigurations = () => (dispatch: AppDispatch, getState: () => RootState) => api(getState) .get('/api/pleroma/frontend_configurations') - .then(({ data }) => data); + .then((response) => response.json()).then((data) => data); /** Conditionally fetches Soapbox config depending on backend features */ const fetchSoapboxConfig = (host: string | null = null) => diff --git a/src/actions/status-quotes.test.ts b/src/actions/status-quotes.test.ts deleted file mode 100644 index b11085fec..000000000 --- a/src/actions/status-quotes.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; -import { StatusListRecord } from 'soapbox/reducers/status-lists.ts'; - -import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes.ts'; - -const status = { - account: { - id: 'ABDSjI3Q0R8aDaz1U0', - }, - content: 'quoast', - id: 'AJsajx9hY4Q7IKQXEe', - pleroma: { - quote: { - content: '

10

', - id: 'AJmoVikzI3SkyITyim', - }, - }, -}; - -const statusId = 'AJmoVikzI3SkyITyim'; - -describe('fetchStatusQuotes()', () => { - let store: ReturnType; - - beforeEach(() => { - const state = { ...rootState, me: '1234' }; - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const quotes = await import('soapbox/__fixtures__/status-quotes.json'); - - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, { - link: `; rel='prev'`, - }); - }); - }); - - it('should fetch quotes from the API', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_FETCH_REQUEST', statusId }, - { type: 'POLLS_IMPORT', polls: [] }, - { type: 'ACCOUNTS_IMPORT', accounts: [status.account] }, - { type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false }, - { type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null }, - ]; - await store.dispatch(fetchStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_FETCH_REQUEST', statusId }, - { type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') }, - ]; - await store.dispatch(fetchStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); -}); - -describe('expandStatusQuotes()', () => { - let store: ReturnType; - - describe('without a url', () => { - beforeEach(() => { - const state = { - ...rootState, - me: '1234', - status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }), - }; - - store = mockStore(state); - }); - - it('should do nothing', async() => { - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual([]); - }); - }); - - describe('with a url', () => { - beforeEach(() => { - const state = { - ...rootState, - status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }), - me: '1234', - }; - - store = mockStore(state); - }); - - describe('with a successful API request', () => { - beforeEach(async () => { - const quotes = await import('soapbox/__fixtures__/status-quotes.json'); - - __stub((mock) => { - mock.onGet('example').reply(200, quotes, { - link: `; rel='prev'`, - }); - }); - }); - - it('should fetch quotes from the API', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId }, - { type: 'POLLS_IMPORT', polls: [] }, - { type: 'ACCOUNTS_IMPORT', accounts: [status.account] }, - { type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false }, - { type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null }, - ]; - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - - describe('with an unsuccessful API request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('example').networkError(); - }); - }); - - it('should dispatch failed action', async() => { - const expectedActions = [ - { type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId }, - { type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') }, - ]; - await store.dispatch(expandStatusQuotes(statusId)); - const actions = store.getActions(); - - expect(actions).toEqual(expectedActions); - }); - }); - }); -}); diff --git a/src/actions/status-quotes.ts b/src/actions/status-quotes.ts index 2bb83a721..c34d673f4 100644 --- a/src/actions/status-quotes.ts +++ b/src/actions/status-quotes.ts @@ -1,4 +1,4 @@ -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedStatuses } from './importer/index.ts'; @@ -25,14 +25,15 @@ export const fetchStatusQuotes = (statusId: string) => type: STATUS_QUOTES_FETCH_REQUEST, }); - return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); + return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); return dispatch({ type: STATUS_QUOTES_FETCH_SUCCESS, statusId, - statuses: response.data, - next: next ? next.uri : null, + statuses: data, + next, }); }).catch(error => { dispatch({ @@ -56,14 +57,14 @@ export const expandStatusQuotes = (statusId: string) => statusId, }); - return api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); + return api(getState).get(url).then(async (response) => { + const data = await response.json(); + dispatch(importFetchedStatuses(data)); dispatch({ type: STATUS_QUOTES_EXPAND_SUCCESS, statusId, - statuses: response.data, - next: next ? next.uri : null, + statuses: data, + next: response.next(), }); }).catch(error => { dispatch({ diff --git a/src/actions/statuses.test.ts b/src/actions/statuses.test.ts deleted file mode 100644 index 641b9d48d..000000000 --- a/src/actions/statuses.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { fromJS, Map as ImmutableMap } from 'immutable'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { STATUSES_IMPORT } from 'soapbox/actions/importer/index.ts'; -import { __stub } from 'soapbox/api/index.ts'; -import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx'; -import { normalizeStatus } from 'soapbox/normalizers/status.ts'; - -import { deleteStatus, fetchContext } from './statuses.ts'; - -describe('fetchContext()', () => { - it('handles Mitra context', async () => { - const statuses = await import('soapbox/__fixtures__/mitra-context.json'); - - __stub(mock => { - mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context') - .reply(200, statuses); - }); - - const store = mockStore(rootState); - await store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70')); - const actions = store.getActions(); - - expect(actions[3].type).toEqual(STATUSES_IMPORT); - expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05'); - }); -}); - -describe('deleteStatus()', () => { - let store: ReturnType; - - describe('if logged out', () => { - beforeEach(() => { - const state = { ...rootState, 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 = { - ...rootState, - me: '1234', - statuses: fromJS({ - [statusId]: cachedStatus, - }) as any, - }; - - store = mockStore(state); - }); - - describe('with a successful API request', () => { - let status: any; - - beforeEach(async () => { - status = await import('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, - id: 'compose-modal', - }, - { type: 'MODAL_CLOSE', modalType: 'COMPOSE', modalProps: undefined }, - { 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/src/actions/statuses.ts b/src/actions/statuses.ts index 8d00cf00b..0c1d942e8 100644 --- a/src/actions/statuses.ts +++ b/src/actions/statuses.ts @@ -2,7 +2,7 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; import { shouldHaveCard } from 'soapbox/utils/status.ts'; -import api, { getNextLink } from '../api/index.ts'; +import api from '../api/index.ts'; import { setComposeToStatus } from './compose-status.ts'; import { fetchGroupRelationships } from './groups.ts'; @@ -59,12 +59,13 @@ const createStatus = (params: Record, idempotencyKey: string, statu return (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId }); - return api(getState).request({ - url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, - method: statusId === null ? 'post' : 'put', - data: params, - headers: { 'Idempotency-Key': idempotencyKey }, - }).then(({ data: status }) => { + const method = statusId === null ? 'POST' : 'PUT'; + const path = statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`; + const headers = { 'Idempotency-Key': idempotencyKey }; + + return api(getState).request(method, path, params, { headers }).then(async (response) => { + const status = await response.json(); + // The backend might still be processing the rich media attachment if (!status.card && shouldHaveCard(status)) { status.expectsCard = true; @@ -78,9 +79,9 @@ const createStatus = (params: Record, idempotencyKey: string, statu const delay = 1000; const poll = (retries = 5) => { - api(getState).get(`/api/v1/statuses/${status.id}`).then(response => { - if (response.data?.card) { - dispatch(importFetchedStatus(response.data)); + api(getState).get(`/api/v1/statuses/${status.id}`).then((response) => response.json()).then((data) => { + if (data?.card) { + dispatch(importFetchedStatus(data)); } else if (retries > 0 && response.status === 200) { setTimeout(() => poll(retries - 1), delay); } @@ -107,9 +108,9 @@ const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootS dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); - api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { + api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); - dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type, false)); + dispatch(setComposeToStatus(status, data.text, data.spoiler_text, data.content_type, false)); dispatch(openModal('COMPOSE')); }).catch(error => { dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); @@ -123,7 +124,7 @@ const fetchStatus = (id: string) => { dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading }); - return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => { + return api(getState).get(`/api/v1/statuses/${id}`).then((response) => response.json()).then((status) => { dispatch(importFetchedStatus(status)); if (status.group) { dispatch(fetchGroupRelationships([status.group.id])); @@ -150,12 +151,12 @@ const deleteStatus = (id: string, withRedraft = false) => { return api(getState) .delete(`/api/v1/statuses/${id}`) - .then(response => { + .then((response) => response.json()).then((data) => { 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(setComposeToStatus(status, data.text, data.spoiler_text, data.pleroma?.content_type, withRedraft)); dispatch(openModal('COMPOSE')); } }) @@ -172,7 +173,7 @@ const fetchContext = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: CONTEXT_FETCH_REQUEST, id }); - return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => { + return api(getState).get(`/api/v1/statuses/${id}/context`).then((response) => response.json()).then((context) => { if (Array.isArray(context)) { // Mitra: returns a list of statuses dispatch(importFetchedStatuses(context)); @@ -198,29 +199,33 @@ const fetchContext = (id: string) => const fetchNext = (statusId: string, next: string) => async(dispatch: AppDispatch, getState: () => RootState) => { const response = await api(getState).get(next); - dispatch(importFetchedStatuses(response.data)); + const data = await response.json(); + + dispatch(importFetchedStatuses(data)); dispatch({ type: CONTEXT_FETCH_SUCCESS, id: statusId, ancestors: [], - descendants: response.data, + descendants: data, }); - return { next: getNextLink(response) }; + return { next: response.pagination().next }; }; const fetchAncestors = (id: string) => async(dispatch: AppDispatch, getState: () => RootState) => { const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`); - dispatch(importFetchedStatuses(response.data)); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); return response; }; const fetchDescendants = (id: string) => async(dispatch: AppDispatch, getState: () => RootState) => { const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`); - dispatch(importFetchedStatuses(response.data)); + const data = await response.json(); + dispatch(importFetchedStatuses(data)); return response; }; @@ -230,7 +235,8 @@ const fetchStatusWithContext = (id: string) => if (features.paginatedContext) { await dispatch(fetchStatus(id)); - const responses = await Promise.all([ + + const [ancestors, descendants] = await Promise.all([ dispatch(fetchAncestors(id)), dispatch(fetchDescendants(id)), ]); @@ -238,18 +244,17 @@ const fetchStatusWithContext = (id: string) => dispatch({ type: CONTEXT_FETCH_SUCCESS, id, - ancestors: responses[0].data, - descendants: responses[1].data, + ancestors: await ancestors.json(), + descendants: await descendants.json(), }); - const next = getNextLink(responses[1]); - return { next }; + return descendants.pagination(); } else { await Promise.all([ dispatch(fetchContext(id)), dispatch(fetchStatus(id)), ]); - return { next: undefined }; + return { next: null, prev: null }; } }; @@ -322,11 +327,11 @@ const translateStatus = (id: string, lang?: string) => (dispatch: AppDispatch, g api(getState).post(`/api/v1/statuses/${id}/translate`, { lang, // Mastodon API target_language: lang, // HACK: Rebased and Pleroma compatibility - }).then(response => { + }).then((response) => response.json()).then((data) => { dispatch({ type: STATUS_TRANSLATE_SUCCESS, id, - translation: response.data, + translation: data, }); }).catch(error => { dispatch({ diff --git a/src/actions/suggestions.ts b/src/actions/suggestions.ts index 5065100b6..2a836bda4 100644 --- a/src/actions/suggestions.ts +++ b/src/actions/suggestions.ts @@ -1,7 +1,7 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchRelationships } from './accounts.ts'; import { importFetchedAccounts } from './importer/index.ts'; @@ -23,7 +23,7 @@ const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL'; const fetchSuggestionsV1 = (params: Record = {}) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true }); - return api(getState).get('/api/v1/suggestions', { params }).then(({ data: accounts }) => { + return api(getState).get('/api/v1/suggestions', { searchParams: params }).then((response) => response.json()).then((accounts) => { dispatch(importFetchedAccounts(accounts)); dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, accounts, skipLoading: true }); return accounts; @@ -39,10 +39,10 @@ const fetchSuggestionsV2 = (params: Record = {}) => dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true }); - return api(getState).get(next ? next : '/api/v2/suggestions', next ? {} : { params }).then((response) => { - const suggestions: APIEntity[] = response.data; + return api(getState).get(next ?? '/api/v2/suggestions', next ? {} : { searchParams: params }).then(async (response) => { + const suggestions: APIEntity[] = await response.json(); const accounts = suggestions.map(({ account }) => account); - const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri; + const next = response.next(); dispatch(importFetchedAccounts(accounts)); dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true }); diff --git a/src/actions/tags.ts b/src/actions/tags.ts index 3873bc329..5a2981d51 100644 --- a/src/actions/tags.ts +++ b/src/actions/tags.ts @@ -1,4 +1,4 @@ -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import type { AppDispatch, RootState } from 'soapbox/store.ts'; import type { APIEntity } from 'soapbox/types/entities.ts'; @@ -26,7 +26,7 @@ const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchHashtagRequest()); - api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => { + api(getState).get(`/api/v1/tags/${name}`).then((response) => response.json()).then((data) => { dispatch(fetchHashtagSuccess(name, data)); }).catch(err => { dispatch(fetchHashtagFail(err)); @@ -51,7 +51,7 @@ const fetchHashtagFail = (error: unknown) => ({ const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(followHashtagRequest(name)); - api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => { + api(getState).post(`/api/v1/tags/${name}/follow`).then((response) => response.json()).then((data) => { dispatch(followHashtagSuccess(name, data)); }).catch(err => { dispatch(followHashtagFail(name, err)); @@ -78,7 +78,7 @@ const followHashtagFail = (name: string, error: unknown) => ({ const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(unfollowHashtagRequest(name)); - api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { + api(getState).post(`/api/v1/tags/${name}/unfollow`).then((response) => response.json()).then((data) => { dispatch(unfollowHashtagSuccess(name, data)); }).catch(err => { dispatch(unfollowHashtagFail(name, err)); @@ -105,9 +105,10 @@ const unfollowHashtagFail = (name: string, error: unknown) => ({ const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchFollowedHashtagsRequest()); - api(getState).get('/api/v1/followed_tags').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + api(getState).get('/api/v1/followed_tags').then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(fetchFollowedHashtagsSuccess(data, next)); }).catch(err => { dispatch(fetchFollowedHashtagsFail(err)); }); @@ -137,9 +138,10 @@ const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => Roo dispatch(expandFollowedHashtagsRequest()); - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + api(getState).get(url).then(async (response) => { + const next = response.next(); + const data = await response.json(); + dispatch(expandFollowedHashtagsSuccess(data, next)); }).catch(error => { dispatch(expandFollowedHashtagsFail(error)); }); diff --git a/src/actions/timelines.ts b/src/actions/timelines.ts index 4b7aa6ce1..efbc988e6 100644 --- a/src/actions/timelines.ts +++ b/src/actions/timelines.ts @@ -4,7 +4,7 @@ import { getSettings } from 'soapbox/actions/settings.ts'; import { normalizeStatus } from 'soapbox/normalizers/index.ts'; import { shouldFilter } from 'soapbox/utils/timelines.ts'; -import api, { getNextLink, getPrevLink } from '../api/index.ts'; +import api from '../api/index.ts'; import { fetchGroupRelationships } from './groups.ts'; import { importFetchedStatus, importFetchedStatuses } from './importer/index.ts'; @@ -169,17 +169,20 @@ const expandTimeline = (timelineId: string, path: string, params: Record { - dispatch(importFetchedStatuses(response.data)); + return api(getState).get(path, { searchParams: params }).then(async (response) => { + const { next, prev } = response.pagination(); + const data: APIEntity[] = await response.json(); - const statusesFromGroups = (response.data as Status[]).filter((status) => !!status.group); + dispatch(importFetchedStatuses(data)); + + const statusesFromGroups = (data as Status[]).filter((status) => !!status.group); dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id))); dispatch(expandTimelineSuccess( timelineId, - response.data, - getNextLink(response), - getPrevLink(response), + data, + next, + prev, response.status === 206, isLoadingRecent, isLoadingMore, @@ -267,8 +270,8 @@ const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({ const expandTimelineSuccess = ( timeline: string, statuses: APIEntity[], - next: string | undefined, - prev: string | undefined, + next: string | null, + prev: string | null, partial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean, diff --git a/src/actions/trending-statuses.ts b/src/actions/trending-statuses.ts index e0075b019..c1c88c308 100644 --- a/src/actions/trending-statuses.ts +++ b/src/actions/trending-statuses.ts @@ -1,7 +1,7 @@ import { APIEntity } from 'soapbox/types/entities.ts'; import { getFeatures } from 'soapbox/utils/features.ts'; -import api, { getLinks } from '../api/index.ts'; +import api from '../api/index.ts'; import { importFetchedStatuses } from './importer/index.ts'; @@ -23,13 +23,14 @@ const fetchTrendingStatuses = () => if (!features.trendingStatuses) return; dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST }); - return api(getState).get('/api/v1/trends/statuses').then((response) => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + return api(getState).get('/api/v1/trends/statuses').then(async (response) => { + const next = response.next(); + const data = await response.json(); - const statuses = response.data; + const statuses = data; dispatch(importFetchedStatuses(statuses)); - dispatch(fetchTrendingStatusesSuccess(statuses, next ? next.uri : null)); + dispatch(fetchTrendingStatusesSuccess(statuses, next)); return statuses; }).catch(error => { dispatch(fetchTrendingStatusesFail(error)); @@ -50,13 +51,14 @@ const fetchTrendingStatusesFail = (error: unknown) => ({ const expandTrendingStatuses = (path: string) => (dispatch: AppDispatch, getState: () => RootState) => { - api(getState).get(path).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); + api(getState).get(path).then(async (response) => { + const next = response.next(); + const data = await response.json(); - const statuses = response.data; + const statuses = data; dispatch(importFetchedStatuses(statuses)); - dispatch(expandTrendingStatusesSuccess(statuses, next ? next.uri : null)); + dispatch(expandTrendingStatusesSuccess(statuses, next)); }).catch(error => { dispatch(expandTrendingStatusesFail(error)); }); diff --git a/src/actions/trends.ts b/src/actions/trends.ts index f13e063cf..530b782b6 100644 --- a/src/actions/trends.ts +++ b/src/actions/trends.ts @@ -11,8 +11,8 @@ const fetchTrends = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchTrendsRequest()); - api(getState).get('/api/v1/trends').then(response => { - dispatch(fetchTrendsSuccess(response.data)); + api(getState).get('/api/v1/trends').then((response) => response.json()).then(data => { + dispatch(fetchTrendsSuccess(data)); }).catch(error => dispatch(fetchTrendsFail(error))); }; diff --git a/src/api/HTTPError.ts b/src/api/HTTPError.ts index b3e586315..ea2874dfa 100644 --- a/src/api/HTTPError.ts +++ b/src/api/HTTPError.ts @@ -1,9 +1,11 @@ +import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts'; + export class HTTPError extends Error { - response: Response; + response: MastodonResponse; request: Request; - constructor(response: Response, request: Request) { + constructor(response: MastodonResponse, request: Request) { super(response.statusText); this.response = response; this.request = request; diff --git a/src/api/MastodonClient.ts b/src/api/MastodonClient.ts index 3fa46f4bf..6ad2fce06 100644 --- a/src/api/MastodonClient.ts +++ b/src/api/MastodonClient.ts @@ -2,7 +2,8 @@ import { HTTPError } from './HTTPError.ts'; import { MastodonResponse } from './MastodonResponse.ts'; interface Opts { - searchParams?: URLSearchParams | Record; + searchParams?: URLSearchParams | Record; + onUploadProgress?: (e: ProgressEvent) => void; headers?: Record; signal?: AbortSignal; } @@ -56,7 +57,16 @@ export class MastodonClient { ? opts.searchParams : Object .entries(opts.searchParams) - .map(([key, value]) => ([key, String(value)])); + .reduce<[string, string][]>((acc, [key, value]) => { + if (Array.isArray(value)) { + for (const v of value) { + acc.push([`${key}[]`, String(v)]); + } + } else if (value !== undefined && value !== null) { + acc.push([key, String(value)]); + } + return acc; + }, []); url.search = new URLSearchParams(params).toString(); } @@ -70,7 +80,6 @@ export class MastodonClient { let body: BodyInit | undefined; if (data instanceof FormData) { - headers.set('Content-Type', 'multipart/form-data'); body = data; } else if (data !== undefined) { headers.set('Content-Type', 'application/json'); @@ -84,19 +93,77 @@ export class MastodonClient { body, }); - const response = await this.fetch(request); + const response = opts.onUploadProgress + ? await this.xhr(request, opts) + : MastodonResponse.fromResponse(await this.fetch(request)); if (!response.ok) { throw new HTTPError(response, request); } - // Fix for non-compliant browsers. - // https://developer.mozilla.org/en-US/docs/Web/API/Response/body - if (response.status === 204 || request.method === 'HEAD') { - return new MastodonResponse(null, response); + return response; + } + + /** + * Perform an XHR request from the native `Request` object and get back a `MastodonResponse`. + * This is needed because unfortunately `fetch` does not support upload progress. + */ + private async xhr(request: Request, opts: Opts = {}): Promise { + const xhr = new XMLHttpRequest(); + const { resolve, reject, promise } = Promise.withResolvers(); + + xhr.responseType = 'arraybuffer'; + + xhr.onreadystatechange = () => { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return; + } + + const headers = new Headers( + xhr.getAllResponseHeaders() + .trim() + .split(/[\r\n]+/) + .map((line): [string, string] => { + const [name, ...rest] = line.split(': '); + const value = rest.join(': '); + return [name, value]; + }), + ); + + const response = new MastodonResponse(xhr.response, { + status: xhr.status, + statusText: xhr.statusText, + headers, + }); + + resolve(response); + }; + + xhr.onerror = () => { + reject(new TypeError('Network request failed')); + }; + + xhr.onabort = () => { + reject(new DOMException('The request was aborted', 'AbortError')); + }; + + if (opts.onUploadProgress) { + xhr.upload.onprogress = opts.onUploadProgress; } - return new MastodonResponse(response.body, response); + if (opts.signal) { + opts.signal.addEventListener('abort', () => xhr.abort(), { once: true }); + } + + xhr.open(request.method, request.url, true); + + for (const [name, value] of request.headers) { + xhr.setRequestHeader(name, value); + } + + xhr.send(await request.arrayBuffer()); + + return promise; } } \ No newline at end of file diff --git a/src/api/MastodonResponse.ts b/src/api/MastodonResponse.ts index fb54008b0..b85fe7c49 100644 --- a/src/api/MastodonResponse.ts +++ b/src/api/MastodonResponse.ts @@ -1,16 +1,87 @@ import LinkHeader from 'http-link-header'; +import { z } from 'zod'; + +/** Mastodon JSON error response. */ +export interface MastodonError { + /** Error message in plaintext, to be displayed in the UI. */ + error: string; + /** Map of field validation errors. See: https://github.com/mastodon/mastodon/pull/15803 */ + detail?: Record; +} + +/** Parsed Mastodon `Link` header. */ +export interface MastodonLink { + rel: string; + uri: string; +} export class MastodonResponse extends Response { - /** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */ - pagination(): { prev?: string; next?: string } { + /** Construct a `MastodonResponse` from a regular `Response` object. */ + static fromResponse(response: Response): MastodonResponse { + // Fix for non-compliant browsers. + // https://developer.mozilla.org/en-US/docs/Web/API/Response/body + if (response.status === 204) { + return new MastodonResponse(null, response); + } + + return new MastodonResponse(response.body, response); + } + + /** Parses the `Link` header and returns an array of URLs and their rel values. */ + links(): MastodonLink[] { const header = this.headers.get('link'); - const links = header ? new LinkHeader(header) : undefined; + + if (header) { + return new LinkHeader(header).refs; + } else { + return []; + } + } + + /** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */ + pagination(): { prev: string | null; next: string | null } { + const links = this.links(); return { - next: links?.refs.find((link) => link.rel === 'next')?.uri, - prev: links?.refs.find((link) => link.rel === 'prev')?.uri, + next: links.find((link) => link.rel === 'next')?.uri ?? null, + prev: links.find((link) => link.rel === 'prev')?.uri ?? null, }; } + /** Returns the `next` URI from the `Link` header, if applicable. */ + next(): string | null { + const links = this.links(); + return links.find((link) => link.rel === 'next')?.uri ?? null; + } + + /** Returns the `prev` URI from the `Link` header, if applicable. */ + prev(): string | null { + const links = this.links(); + return links.find((link) => link.rel === 'prev')?.uri ?? null; + } + + /** Extracts the error JSON from the response body, if possible. Otherwise returns `null`. */ + async error(): Promise { + const data = await this.json(); + const result = MastodonResponse.errorSchema().safeParse(data); + + if (result.success) { + return result.data; + } else { + return null; + } + } + + /** Validates the error response schema. */ + private static errorSchema(): z.ZodType { + return z.object({ + error: z.string(), + detail: z.record( + z.string(), + z.object({ error: z.string(), description: z.string() }).array(), + ).optional(), + }); + } + } diff --git a/src/api/__mocks__/index.ts b/src/api/__mocks__/index.ts deleted file mode 100644 index ff5fba2c2..000000000 --- a/src/api/__mocks__/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import LinkHeader from 'http-link-header'; -import { vi } from 'vitest'; - -import type { AxiosInstance, AxiosResponse } from 'axios'; - -const api = await vi.importActual('../index') as Record; -let mocks: Array = []; - -export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func); -export const __clear = (): Function[] => mocks = []; - -const setupMock = (axios: AxiosInstance) => { - const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); - mocks.map(func => func(mock)); -}; - -export const getLinks = (response: AxiosResponse): LinkHeader => { - return new LinkHeader(response.headers?.link); -}; - -export const getNextLink = (response: AxiosResponse) => { - const nextLink = new LinkHeader(response.headers?.link); - return nextLink.refs.find(link => link.rel === 'next')?.uri; -}; - -export const getPrevLink = (response: AxiosResponse) => { - const prevLink = new LinkHeader(response.headers?.link); - return prevLink.refs.find(link => link.rel === 'prev')?.uri; -}; - -export const baseClient = (...params: any[]) => { - const axios = api.baseClient(...params); - setupMock(axios); - return axios; -}; - -export default (...params: any[]) => { - const axios = api.default(...params); - setupMock(axios); - return axios; -}; diff --git a/src/api/hooks/groups/useGroup.test.ts b/src/api/hooks/groups/useGroup.test.ts deleted file mode 100644 index 4212bf7c2..000000000 --- a/src/api/hooks/groups/useGroup.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroup } from 'soapbox/jest/factory.ts'; -import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import { useGroup } from './useGroup.ts'; - -const group = buildGroup({ id: '1', display_name: 'soapbox' }); - -describe('useGroup hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${group.id}`).reply(200, group); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(() => useGroup(group.id)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.group?.id).toBe(group.id); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${group.id}`).networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(() => useGroup(group.id)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.group).toBeUndefined(); - }); - }); -}); \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupLookup.test.ts b/src/api/hooks/groups/useGroupLookup.test.ts deleted file mode 100644 index f000540e5..000000000 --- a/src/api/hooks/groups/useGroupLookup.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroup } from 'soapbox/jest/factory.ts'; -import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import { useGroupLookup } from './useGroupLookup.ts'; - -const group = buildGroup({ id: '1', slug: 'soapbox' }); -const state = { - ...rootState, - instance: { - ...rootState.instance, - version: '3.4.1 (compatible; TruthSocial 1.0.0)', - }, -}; - -describe('useGroupLookup hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/lookup?name=${group.slug}`).reply(200, group); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(() => useGroupLookup(group.slug), undefined, state); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.entity?.id).toBe(group.id); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/lookup?name=${group.slug}`).networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(() => useGroupLookup(group.slug), undefined, state); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.entity).toBeUndefined(); - }); - }); -}); \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMedia.test.ts b/src/api/hooks/groups/useGroupMedia.test.ts deleted file mode 100644 index 88d9cc281..000000000 --- a/src/api/hooks/groups/useGroupMedia.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildStatus } from 'soapbox/jest/factory.ts'; -import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import { useGroupMedia } from './useGroupMedia.ts'; - -const status = buildStatus(); -const groupId = '1'; - -describe('useGroupMedia hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).reply(200, [status]); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(() => useGroupMedia(groupId)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.entities.length).toBe(1); - expect(result.current.entities[0].id).toBe(status.id); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(() => useGroupMedia(groupId)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.entities.length).toBe(0); - expect(result.current.isError).toBeTruthy(); - }); - }); -}); \ No newline at end of file diff --git a/src/api/hooks/groups/useGroupMembers.test.ts b/src/api/hooks/groups/useGroupMembers.test.ts deleted file mode 100644 index 02971b2cb..000000000 --- a/src/api/hooks/groups/useGroupMembers.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroupMember } from 'soapbox/jest/factory.ts'; -import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { GroupRoles } from 'soapbox/schemas/group-member.ts'; - -import { useGroupMembers } from './useGroupMembers.ts'; - -const groupMember = buildGroupMember(); -const groupId = '1'; - -describe('useGroupMembers hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).reply(200, [groupMember]); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groupMembers.length).toBe(1); - expect(result.current.groupMembers[0].id).toBe(groupMember.id); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN)); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groupMembers.length).toBe(0); - expect(result.current.isError).toBeTruthy(); - }); - }); -}); \ No newline at end of file diff --git a/src/api/hooks/groups/useGroups.test.ts b/src/api/hooks/groups/useGroups.test.ts deleted file mode 100644 index 8a91db69c..000000000 --- a/src/api/hooks/groups/useGroups.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroup } from 'soapbox/jest/factory.ts'; -import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { instanceV1Schema } from 'soapbox/schemas/instance.ts'; - -import { useGroups } from './useGroups.ts'; - -const group = buildGroup({ id: '1', display_name: 'soapbox' }); -const store = { - instance: instanceV1Schema.parse({ - version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', - }), -}; - -describe('useGroups hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups').reply(200, [group]); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(useGroups, undefined, store); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groups).toHaveLength(1); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups').networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(useGroups, undefined, store); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groups).toHaveLength(0); - }); - }); -}); \ No newline at end of file diff --git a/src/api/hooks/groups/usePendingGroups.test.ts b/src/api/hooks/groups/usePendingGroups.test.ts deleted file mode 100644 index 1a380f459..000000000 --- a/src/api/hooks/groups/usePendingGroups.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { Entities } from 'soapbox/entity-store/entities.ts'; -import { buildAccount, buildGroup } from 'soapbox/jest/factory.ts'; -import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { instanceV1Schema } from 'soapbox/schemas/instance.ts'; - -import { usePendingGroups } from './usePendingGroups.ts'; - -const id = '1'; -const group = buildGroup({ id, display_name: 'soapbox' }); -const store = { - instance: instanceV1Schema.parse({ - version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', - }), - me: '1', - entities: { - [Entities.ACCOUNTS]: { - store: { - [id]: buildAccount({ - id, - acct: 'tiger', - display_name: 'Tiger', - avatar: 'test.jpg', - verified: true, - }), - }, - lists: {}, - }, - }, -}; - -describe('usePendingGroups hook', () => { - describe('with a successful request', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups').reply(200, [group]); - }); - }); - - it('is successful', async () => { - const { result } = renderHook(usePendingGroups, undefined, store); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groups).toHaveLength(1); - }); - }); - - describe('with an unsuccessful query', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups').networkError(); - }); - }); - - it('is has error state', async() => { - const { result } = renderHook(usePendingGroups, undefined, store); - - await waitFor(() => expect(result.current.isFetching).toBe(false)); - - expect(result.current.groups).toHaveLength(0); - }); - }); -}); \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index 0898ab576..f56fa9a4c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,50 +1,14 @@ -/** - * API: HTTP client and utilities. - * @see {@link https://github.com/axios/axios} - * @module soapbox/api - */ - -import axios, { type AxiosInstance, type AxiosResponse } from 'axios'; -import LinkHeader from 'http-link-header'; import { createSelector } from 'reselect'; -import * as BuildConfig from 'soapbox/build-config.ts'; +import { MastodonClient } from 'soapbox/api/MastodonClient.ts'; import { selectAccount } from 'soapbox/selectors/index.ts'; import { RootState } from 'soapbox/store.ts'; -import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth.ts'; - -import type MockAdapter from 'axios-mock-adapter'; - -/** - Parse Link headers, mostly for pagination. - @see {@link https://www.npmjs.com/package/http-link-header} - @param {object} response - Axios response object - @returns {object} Link object - */ -export const getLinks = (response: AxiosResponse): LinkHeader => { - return new LinkHeader(response.headers?.link); -}; - -export const getNextLink = (response: AxiosResponse): string | undefined => { - return getLinks(response).refs.find(link => link.rel === 'next')?.uri; -}; - -export const getPrevLink = (response: AxiosResponse): string | undefined => { - return getLinks(response).refs.find(link => link.rel === 'prev')?.uri; -}; +import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth.ts'; const getToken = (state: RootState, authType: string) => { return authType === 'app' ? getAppToken(state) : getAccessToken(state); }; -const maybeParseJSON = (data: string) => { - try { - return JSON.parse(data); - } catch (Exception) { - return data; - } -}; - const getAuthBaseURL = createSelector([ (state: RootState, me: string | false | null) => me ? selectAccount(state, me)?.url : undefined, (state: RootState, _me: string | false | null) => state.auth.me, @@ -53,55 +17,23 @@ const getAuthBaseURL = createSelector([ return baseURL !== window.location.origin ? baseURL : ''; }); -/** - * Base client for HTTP requests. - * @param {string} accessToken - * @param {string} baseURL - * @returns {object} Axios instance - */ +/** Base client for HTTP requests. */ export const baseClient = ( accessToken?: string | null, baseURL: string = '', - nostrSign = false, -): AxiosInstance => { - const headers: Record = {}; - - if (accessToken) { - headers.Authorization = `Bearer ${accessToken}`; - } - - if (nostrSign) { - headers['X-Nostr-Sign'] = 'true'; - } - - return axios.create({ - // When BACKEND_URL is set, always use it. - baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL, - headers, - transformResponse: [maybeParseJSON], - }); +): MastodonClient => { + return new MastodonClient(baseURL, accessToken || undefined); }; /** - * Stateful API client. - * Uses credentials from the Redux store if available. - * @param {function} getState - Must return the Redux state - * @param {string} authType - Either 'user' or 'app' - * @returns {object} Axios instance - */ -export default (getState: () => RootState, authType: string = 'user'): AxiosInstance => { + * Stateful API client. + * Uses credentials from the Redux store if available. + */ +export default (getState: () => RootState, authType: string = 'user'): MastodonClient => { const state = getState(); const accessToken = getToken(state, authType); const me = state.me; const baseURL = me ? getAuthBaseURL(state, me) : ''; - const relayUrl = state.instance?.nostr?.relay; - const pubkey = state.instance?.nostr?.pubkey; - const nostrSign = Boolean(relayUrl && pubkey); - - return baseClient(accessToken, baseURL, nostrSign); + return baseClient(accessToken, baseURL); }; - -// The Jest mock exports these, so they're needed for TypeScript. -export const __stub = (_func: (mock: MockAdapter) => void) => 0; -export const __clear = (): Function[] => []; diff --git a/src/entity-store/hooks/types.ts b/src/entity-store/hooks/types.ts index 32a2e64ea..4c2fa202b 100644 --- a/src/entity-store/hooks/types.ts +++ b/src/entity-store/hooks/types.ts @@ -32,7 +32,7 @@ interface EntityCallbacks { /** * Passed into hooks to make requests. - * Must return an Axios response. + * Must return a Response object. */ type EntityFn = (value: T) => Promise diff --git a/src/features/account/components/header.tsx b/src/features/account/components/header.tsx index 952a02146..f6195fd31 100644 --- a/src/features/account/components/header.tsx +++ b/src/features/account/components/header.tsx @@ -19,7 +19,6 @@ import userCheckIcon from '@tabler/icons/outline/user-check.svg'; import userXIcon from '@tabler/icons/outline/user-x.svg'; import userIcon from '@tabler/icons/outline/user.svg'; import { useMutation } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; import { List as ImmutableList } from 'immutable'; import { nip19 } from 'nostr-tools'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -33,6 +32,7 @@ import { initMuteModal } from 'soapbox/actions/mutes.ts'; import { initReport, ReportableEntities } from 'soapbox/actions/reports.ts'; import { setSearchAccount } from 'soapbox/actions/search.ts'; import { getSettings } from 'soapbox/actions/settings.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import { useFollow } from 'soapbox/api/hooks/index.ts'; import Badge from 'soapbox/components/badge.tsx'; import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu/index.ts'; @@ -121,9 +121,10 @@ const Header: React.FC = ({ account }) => { const createAndNavigateToChat = useMutation({ mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId), - onError: (error: AxiosError) => { - const data = error.response?.data as any; - toast.error(data?.error); + onError: (error) => { + if (error instanceof HTTPError) { + toast.showAlertForError(error); + } }, onSuccess: async (response) => { const data = await response.json(); diff --git a/src/features/admin/manage-ditto-server.tsx b/src/features/admin/manage-ditto-server.tsx index 1888abf5d..1ccf65245 100644 --- a/src/features/admin/manage-ditto-server.tsx +++ b/src/features/admin/manage-ditto-server.tsx @@ -1,4 +1,3 @@ -import { AxiosError } from 'axios'; import React, { useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -150,7 +149,8 @@ const ManageDittoServer: React.FC = () => { try { const response = await dispatch(uploadMedia(data)); - const attachment = normalizeAttachment(response.data); + const json = await response.json(); + const attachment = normalizeAttachment(json); if (attachment.type !== 'image') { throw new Error('Only images supported.'); @@ -166,11 +166,9 @@ const ManageDittoServer: React.FC = () => { setThumbnailLoading(false); e.target.value = ''; - if (err instanceof AxiosError) { - toast.error(err.response?.data?.error || 'An error occurred'); - return; + if (err instanceof HTTPError) { + toast.showAlertForError(err); } - toast.error((err as Error)?.message || 'An error occurred'); } }; }; @@ -268,7 +266,8 @@ const ScreenshotInput: StreamfieldComponent = ({ value, onChange }) try { const response = await dispatch(uploadMedia(data)); - const attachment = normalizeAttachment(response.data); + const json = await response.json(); + const attachment = normalizeAttachment(json); if (attachment.type !== 'image') { throw new Error('Only images supported.'); @@ -289,11 +288,9 @@ const ScreenshotInput: StreamfieldComponent = ({ value, onChange }) setLoading(false); e.target.value = ''; - if (err instanceof AxiosError) { - toast.error(err.response?.data?.error || 'An error occurred'); - return; + if (err instanceof HTTPError) { + toast.showAlertForError(err); } - toast.error((err as Error)?.message || 'An error occurred'); } }; }; diff --git a/src/features/admin/tabs/dashboard.tsx b/src/features/admin/tabs/dashboard.tsx index 68f05a050..a3d3e4378 100644 --- a/src/features/admin/tabs/dashboard.tsx +++ b/src/features/admin/tabs/dashboard.tsx @@ -27,21 +27,21 @@ const Dashboard: React.FC = () => { const { account } = useOwnAccount(); const handleSubscribersClick: React.MouseEventHandler = e => { - dispatch(getSubscribersCsv()).then(({ data }) => { + dispatch(getSubscribersCsv()).then((response) => response.json()).then((data) => { download(data, 'subscribers.csv'); }).catch(() => {}); e.preventDefault(); }; const handleUnsubscribersClick: React.MouseEventHandler = e => { - dispatch(getUnsubscribersCsv()).then(({ data }) => { + dispatch(getUnsubscribersCsv()).then((response) => response.json()).then((data) => { download(data, 'unsubscribers.csv'); }).catch(() => {}); e.preventDefault(); }; const handleCombinedClick: React.MouseEventHandler = e => { - dispatch(getCombinedCsv()).then(({ data }) => { + dispatch(getCombinedCsv()).then((response) => response.json()).then((data) => { download(data, 'combined.csv'); }).catch(() => {}); e.preventDefault(); diff --git a/src/features/auth-login/components/captcha.tsx b/src/features/auth-login/components/captcha.tsx index 555f7db6f..ce444cb00 100644 --- a/src/features/auth-login/components/captcha.tsx +++ b/src/features/auth-login/components/captcha.tsx @@ -8,8 +8,6 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import type { AxiosResponse } from 'axios'; - const noOp = () => {}; const messages = defineMessages({ @@ -44,8 +42,8 @@ const CaptchaField: React.FC = ({ const [refresh, setRefresh] = useState(undefined); const getCaptcha = () => { - dispatch(fetchCaptcha()).then((response: AxiosResponse) => { - const captcha = ImmutableMap(response.data); + dispatch(fetchCaptcha()).then((response) => response.json()).then((data) => { + const captcha = ImmutableMap(data); setCaptcha(captcha); onFetch(captcha); }).catch((error: Error) => { diff --git a/src/features/auth-login/components/login-page.tsx b/src/features/auth-login/components/login-page.tsx index 9fcdb9e7c..62e972686 100644 --- a/src/features/auth-login/components/login-page.tsx +++ b/src/features/auth-login/components/login-page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Redirect } from 'react-router-dom'; -import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth.ts'; +import { logIn, verifyCredentials, switchAccount, MfaRequiredError } from 'soapbox/actions/auth.ts'; import { fetchInstance } from 'soapbox/actions/instance.ts'; import { closeModal, openModal } from 'soapbox/actions/modals.ts'; import { BigCard } from 'soapbox/components/big-card.tsx'; @@ -16,8 +16,6 @@ import ConsumersList from './consumers-list.tsx'; import LoginForm from './login-form.tsx'; import OtpAuthForm from './otp-auth-form.tsx'; -import type { AxiosError } from 'axios'; - const LoginPage = () => { const dispatch = useAppDispatch(); @@ -53,11 +51,10 @@ const LoginPage = () => { } else { setShouldRedirect(true); } - }).catch((error: AxiosError) => { - const data: any = error.response?.data; - if (data?.error === 'mfa_required') { + }).catch((error) => { + if (error instanceof MfaRequiredError) { setMfaAuthNeeded(true); - setMfaToken(data.mfa_token); + setMfaToken(error.token); } setIsLoading(false); }); diff --git a/src/features/auth-login/components/password-reset-confirm.test.tsx b/src/features/auth-login/components/password-reset-confirm.test.tsx deleted file mode 100644 index 513d85ff8..000000000 --- a/src/features/auth-login/components/password-reset-confirm.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Route, Switch } from 'react-router-dom'; -import { describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import PasswordResetConfirm from './password-reset-confirm.tsx'; - -const TestableComponent = () => ( - - - Homepage {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} - -); - -describe('', () => { - it('handles successful responses from the API', async() => { - __stub(mock => { - mock.onPost('/api/v1/truth/password_reset/confirm') - .reply(200, {}); - }); - - render( - , - {}, - null, - { initialEntries: ['/edit'] }, - ); - - fireEvent.submit( - screen.getByTestId('form'), { - preventDefault: () => {}, - }, - ); - - await waitFor(() => { - expect(screen.getByTestId('home')).toHaveTextContent('Homepage'); - expect(screen.queryByTestId('form-group-error')).not.toBeInTheDocument(); - }); - }); - - it('handles failed responses from the API', async() => { - __stub(mock => { - mock.onPost('/api/v1/truth/password_reset/confirm') - .reply(403, {}); - }); - - render( - , - {}, - null, - { initialEntries: ['/edit'] }, - ); - - await fireEvent.submit( - screen.getByTestId('form'), { - preventDefault: () => {}, - }, - ); - - await waitFor(() => { - expect(screen.queryByTestId('home')).not.toBeInTheDocument(); - expect(screen.queryByTestId('form-group-error')).toBeInTheDocument(); - }); - }); -}); diff --git a/src/features/auth-login/components/registration-form.tsx b/src/features/auth-login/components/registration-form.tsx index 63b0b4cf7..902338704 100644 --- a/src/features/auth-login/components/registration-form.tsx +++ b/src/features/auth-login/components/registration-form.tsx @@ -1,6 +1,5 @@ import atIcon from '@tabler/icons/outline/at.svg'; import checkIcon from '@tabler/icons/outline/check.svg'; -import axios from 'axios'; import { debounce } from 'es-toolkit'; import { Map as ImmutableMap } from 'immutable'; import { useState, useRef, useCallback } from 'react'; @@ -71,12 +70,12 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const [passwordConfirmation, setPasswordConfirmation] = useState(''); const [passwordMismatch, setPasswordMismatch] = useState(false); - const source = useRef(axios.CancelToken.source()); + const controllerRef = useRef(new AbortController()); - const refreshCancelToken = () => { - source.current.cancel(); - source.current = axios.CancelToken.source(); - return source.current; + const refreshController = () => { + controllerRef.current.abort(); + controllerRef.current = new AbortController(); + return controllerRef.current; }; const updateParams = (map: any) => { @@ -90,7 +89,6 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const onUsernameChange: React.ChangeEventHandler = e => { updateParams({ username: e.target.value }); setUsernameUnavailable(false); - source.current.cancel(); const domain = params.get('domain'); usernameAvailable(e.target.value, domain ? domains!.find(({ id }) => id === domain)?.domain : undefined); @@ -99,7 +97,6 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const onDomainChange: React.ChangeEventHandler = e => { updateParams({ domain: e.target.value || null }); setUsernameUnavailable(false); - source.current.cancel(); const username = params.get('username'); if (username) { @@ -188,9 +185,9 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const usernameAvailable = useCallback(debounce((username, domain?: string) => { if (!supportsAccountLookup) return; - const source = refreshCancelToken(); + const controller = refreshController(); - dispatch(accountLookup(`${username}${domain ? `@${domain}` : ''}`, source.token)) + dispatch(accountLookup(`${username}${domain ? `@${domain}` : ''}`, controller.signal)) .then(account => { setUsernameUnavailable(!!account); }) diff --git a/src/features/chats/components/chat-message-list.test.tsx b/src/features/chats/components/chat-message-list.test.tsx deleted file mode 100644 index b8477b598..000000000 --- a/src/features/chats/components/chat-message-list.test.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import userEvent from '@testing-library/user-event'; -import { VirtuosoMockContext } from 'react-virtuoso'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { ChatContext } from 'soapbox/contexts/chat-context.tsx'; -import { buildAccount, buildInstance } from 'soapbox/jest/factory.ts'; -import { queryClient, render, rootState, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { normalizeChatMessage } from 'soapbox/normalizers/index.ts'; -import { IChat } from 'soapbox/queries/chats.ts'; -import { ChatMessage } from 'soapbox/types/entities.ts'; - -import ChatMessageList from './chat-message-list.tsx'; - -const chat: IChat = { - accepted: true, - account: buildAccount({ - username: 'username', - verified: true, - id: '1', - acct: 'acct', - avatar: 'avatar', - avatar_static: 'avatar', - display_name: 'my name', - }), - chat_type: 'direct', - created_at: '2020-06-10T02:05:06.000Z', - created_by_account: '2', - discarded_at: null, - id: '14', - last_message: null, - latest_read_message_by_account: [], - latest_read_message_created_at: null, - message_expiration: 1209600, - unread: 5, -}; - -const chatMessages: ChatMessage[] = [ - normalizeChatMessage({ - account_id: '1', - chat_id: '14', - content: 'this is the first chat', - created_at: '2022-09-09T16:02:26.186Z', - emoji_reactions: null, - expiration: 1209600, - id: '1', - unread: false, - pending: false, - }), - normalizeChatMessage({ - account_id: '2', - chat_id: '14', - content: 'this is the second chat', - created_at: '2022-09-09T16:04:26.186Z', - emoji_reactions: null, - expiration: 1209600, - id: '2', - unread: true, - pending: false, - }), -]; - -// Mock scrollIntoView function. -window.HTMLElement.prototype.scrollIntoView = function () { }; -Object.assign(navigator, { - clipboard: { - writeText: () => { }, - }, -}); - -const store = { - ...rootState, - me: '1', - instance: buildInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' }), -}; - -const renderComponentWithChatContext = () => render( - - - - - , - undefined, - store, -); - -beforeEach(() => { - queryClient.clear(); -}); - -describe('', () => { - describe('when the query is loading', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, { - link: null, - }); - }); - }); - - it('displays the skeleton loader', async () => { - renderComponentWithChatContext(); - - expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(5); - - await waitFor(() => { - expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument(); - expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(0); - }); - }); - }); - - describe('when the query is finished loading', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, { - link: null, - }); - }); - }); - - it('displays the intro', async () => { - renderComponentWithChatContext(); - - expect(screen.queryAllByTestId('chat-message-list-intro')).toHaveLength(0); - - await waitFor(() => { - expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument(); - }); - }); - - it('displays the messages', async () => { - renderComponentWithChatContext(); - - expect(screen.queryAllByTestId('chat-message')).toHaveLength(0); - - await waitFor(() => { - expect(screen.queryAllByTestId('chat-message')).toHaveLength(chatMessages.length); - }); - }); - - it('displays the correct menu options depending on the owner of the message', async () => { - renderComponentWithChatContext(); - - await waitFor(() => { - expect(screen.queryAllByTestId('chat-message-menu')).toHaveLength(2); - }); - - // my message - await userEvent.click(screen.queryAllByTestId('chat-message-menu')[0].querySelector('button') as any); - - // other user message - await userEvent.click(screen.queryAllByTestId('chat-message-menu')[1].querySelector('button') as any); - }); - }); -}); diff --git a/src/features/chats/components/chat-pane/chat-pane.test.tsx b/src/features/chats/components/chat-pane/chat-pane.test.tsx deleted file mode 100644 index a4fb419c1..000000000 --- a/src/features/chats/components/chat-pane/chat-pane.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { VirtuosoMockContext } from 'react-virtuoso'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { ChatContext } from 'soapbox/contexts/chat-context.tsx'; -import { StatProvider } from 'soapbox/contexts/stat-context.tsx'; -import chats from 'soapbox/jest/fixtures/chats.json'; -import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import ChatPane from './chat-pane.tsx'; - -const renderComponentWithChatContext = (store = {}) => render( - - - - - - - , - undefined, - store, -); - -describe('', () => { - // describe('when there are no chats', () => { - // let store: ReturnType; - - // beforeEach(() => { - // const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.2.0)'); - // store = mockStore(state); - - // __stub((mock) => { - // mock.onGet('/api/v1/pleroma/chats').reply(200, [], { - // link: null, - // }); - // }); - // }); - - // it('renders the blankslate', async () => { - // renderComponentWithChatContext(store); - - // await waitFor(() => { - // expect(screen.getByTestId('chat-pane-blankslate')).toBeInTheDocument(); - // }); - // }); - // }); - - describe('when the software is not Truth Social', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/pleroma/chats').reply(200, chats, { - link: '; rel=\'prev\'', - }); - }); - }); - - it('does not render the search input', async () => { - renderComponentWithChatContext(); - - await waitFor(() => { - expect(screen.queryAllByTestId('chat-search-input')).toHaveLength(0); - }); - }); - }); -}); diff --git a/src/features/chats/components/chat-search/chat-search.test.tsx b/src/features/chats/components/chat-search/chat-search.test.tsx deleted file mode 100644 index 39cbcb13e..000000000 --- a/src/features/chats/components/chat-search/chat-search.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import userEvent from '@testing-library/user-event'; -import { VirtuosoMockContext } from 'react-virtuoso'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { ChatProvider } from 'soapbox/contexts/chat-context.tsx'; -import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; - -import ChatSearch from './chat-search.tsx'; - -const renderComponent = () => render( - - - - , {/* eslint-disable-line formatjs/no-literal-string-in-jsx */} - , -); - -describe('', () => { - beforeEach(async() => { - renderComponent(); - }); - - it('renders the search input', () => { - expect(screen.getByTestId('search')).toBeInTheDocument(); - }); - - describe('when searching', () => { - describe('with results', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/search').reply(200, [{ - id: '1', - avatar: 'url', - verified: false, - display_name: 'steve', - acct: 'sjobs', - }]); - }); - }); - - it('renders accounts', async() => { - const user = userEvent.setup(); - await user.type(screen.getByTestId('search'), 'ste'); - - await waitFor(() => { - expect(screen.queryAllByTestId('account')).toHaveLength(1); - }); - }); - }); - - describe('without results', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/accounts/search').reply(200, []); - }); - }); - - it('renders accounts', async() => { - const user = userEvent.setup(); - await user.type(screen.getByTestId('search'), 'ste'); - - await waitFor(() => { - expect(screen.getByTestId('no-results')).toBeInTheDocument(); - }); - }); - }); - }); -}); diff --git a/src/features/chats/components/chat-search/chat-search.tsx b/src/features/chats/components/chat-search/chat-search.tsx index 98a735b42..27bb441de 100644 --- a/src/features/chats/components/chat-search/chat-search.tsx +++ b/src/features/chats/components/chat-search/chat-search.tsx @@ -1,11 +1,11 @@ import searchIcon from '@tabler/icons/outline/search.svg'; import xIcon from '@tabler/icons/outline/x.svg'; import { useMutation } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; import { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Icon from 'soapbox/components/ui/icon.tsx'; import Input from 'soapbox/components/ui/input.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; @@ -49,9 +49,10 @@ const ChatSearch = (props: IChatSearch) => { const handleClickOnSearchResult = useMutation({ mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId), - onError: (error: AxiosError) => { - const data = error.response?.data as any; - toast.error(data?.error); + onError: (error) => { + if (error instanceof HTTPError) { + toast.showAlertForError(error); + } }, onSuccess: async (response) => { const data = await response.json(); diff --git a/src/features/chats/components/chat.tsx b/src/features/chats/components/chat.tsx index 821bd7555..86dd7c4c7 100644 --- a/src/features/chats/components/chat.tsx +++ b/src/features/chats/components/chat.tsx @@ -1,9 +1,9 @@ -import { AxiosError } from 'axios'; import clsx from 'clsx'; import { MutableRefObject, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { uploadMedia } from 'soapbox/actions/media.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Stack from 'soapbox/components/ui/stack.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; @@ -71,10 +71,12 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { onSuccess: () => { setErrorMessage(undefined); }, - onError: (error: AxiosError<{ error: string }>, _variables, context) => { - const message = error.response?.data?.error; - setErrorMessage(message || intl.formatMessage(messages.failedToSend)); - setContent(context.prevContent as string); + onError: async (error: unknown, _variables, context) => { + if (error instanceof HTTPError) { + const data = await error.response.error(); + setErrorMessage(data?.error || intl.formatMessage(messages.failedToSend)); + setContent(context.prevContent as string); + } }, }); @@ -158,7 +160,8 @@ const Chat: React.FC = ({ chat, inputRef, className }) => { const data = new FormData(); data.append('file', file); const response = await dispatch(uploadMedia(data, onUploadProgress)); - return normalizeAttachment(response.data); + const json = await response.json(); + return normalizeAttachment(json); }); return Promise.all(promises) diff --git a/src/features/developers/settings-store.tsx b/src/features/developers/settings-store.tsx index ce4e04d66..5b9f3489c 100644 --- a/src/features/developers/settings-store.tsx +++ b/src/features/developers/settings-store.tsx @@ -60,7 +60,7 @@ const SettingsStore: React.FC = () => { pleroma_settings_store: { [FE_NAME]: settings, }, - })).then(response => { + })).then(() => { dispatch({ type: SETTINGS_UPDATE, settings }); setLoading(false); }).catch(error => { diff --git a/src/features/email-confirmation/index.tsx b/src/features/email-confirmation/index.tsx index 5be0f39ff..be82a13d2 100644 --- a/src/features/email-confirmation/index.tsx +++ b/src/features/email-confirmation/index.tsx @@ -37,8 +37,8 @@ const EmailConfirmation = () => { .catch((error) => { setStatus(Statuses.FAIL); - if (error.response.data.error) { - const message = buildErrorMessage(error.response.data.error); + if (error.data.error) { + const message = buildErrorMessage(error.data.error); toast.error(message); } }); diff --git a/src/features/event/components/event-header.tsx b/src/features/event/components/event-header.tsx index 1a3d382d2..0aef22ae0 100644 --- a/src/features/event/components/event-header.tsx +++ b/src/features/event/components/event-header.tsx @@ -132,7 +132,7 @@ const EventHeader: React.FC = ({ status }) => { }; const handleExportClick = () => { - dispatch(fetchEventIcs(status.id)).then(({ data }) => { + dispatch(fetchEventIcs(status.id)).then((response) => response.json()).then((data) => { download(data, 'calendar.ics'); }).catch(() => {}); }; diff --git a/src/features/event/event-discussion.tsx b/src/features/event/event-discussion.tsx index 9de4eef4f..ede6a3aad 100644 --- a/src/features/event/event-discussion.tsx +++ b/src/features/event/event-discussion.tsx @@ -51,7 +51,7 @@ const EventDiscussion: React.FC = (props) => { }); const [isLoaded, setIsLoaded] = useState(!!status); - const [next, setNext] = useState(); + const [next, setNext] = useState(null); const node = useRef(null); const scroller = useRef(null); diff --git a/src/features/external-login/components/external-login-form.tsx b/src/features/external-login/components/external-login-form.tsx index 5060d0831..1e830719d 100644 --- a/src/features/external-login/components/external-login-form.tsx +++ b/src/features/external-login/components/external-login-form.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { externalLogin, loginWithCode } from 'soapbox/actions/external-auth.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Button from 'soapbox/components/ui/button.tsx'; import FormActions from 'soapbox/components/ui/form-actions.tsx'; import FormGroup from 'soapbox/components/ui/form-group.tsx'; @@ -11,8 +12,6 @@ import Spinner from 'soapbox/components/ui/spinner.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import toast from 'soapbox/toast.tsx'; -import type { AxiosError } from 'axios'; - const messages = defineMessages({ instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' }, instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' }, @@ -41,13 +40,14 @@ const ExternalLoginForm: React.FC = () => { dispatch(externalLogin(host)) .then(() => setLoading(false)) - .catch((error: AxiosError) => { + .catch((error: unknown) => { console.error(error); - const status = error.response?.status; + + const status = error instanceof HTTPError ? error.response.status : undefined; if (status) { toast.error(intl.formatMessage(messages.instanceFailed)); - } else if (!status && error.code === 'ERR_NETWORK') { + } else if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_NETWORK') { toast.error(intl.formatMessage(messages.networkFailed)); } diff --git a/src/features/group/components/group-member-list-item.test.tsx b/src/features/group/components/group-member-list-item.test.tsx deleted file mode 100644 index 1817f97ef..000000000 --- a/src/features/group/components/group-member-list-item.test.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroup, buildGroupMember, buildGroupRelationship } from 'soapbox/jest/factory.ts'; -import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { GroupRoles } from 'soapbox/schemas/group-member.ts'; - -import GroupMemberListItem from './group-member-list-item.tsx'; - -describe('', () => { - describe('account rendering', () => { - const accountId = '4'; - const groupMember = buildGroupMember({}, { - id: accountId, - display_name: 'tiger woods', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render the users avatar', async () => { - const group = buildGroup({ - relationship: buildGroupRelationship(), - }); - - render(); - - await waitFor(() => { - expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name); - }); - }); - }); - - describe('role badge', () => { - const accountId = '4'; - const group = buildGroup(); - - describe('when the user is an Owner', () => { - const groupMember = buildGroupMember({ role: GroupRoles.OWNER }, { - id: accountId, - display_name: 'tiger woods', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render the correct badge', async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId('role-badge')).toHaveTextContent('owner'); - }); - }); - }); - - describe('when the user is an Admin', () => { - const groupMember = buildGroupMember({ role: GroupRoles.ADMIN }, { - id: accountId, - display_name: 'tiger woods', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render the correct badge', async () => { - render(); - - await waitFor(() => { - expect(screen.getByTestId('role-badge')).toHaveTextContent('admin'); - }); - }); - }); - - describe('when the user is an User', () => { - const groupMember = buildGroupMember({ role: GroupRoles.USER }, { - id: accountId, - display_name: 'tiger woods', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render no correct badge', async () => { - render(); - - await waitFor(() => { - expect(screen.queryAllByTestId('role-badge')).toHaveLength(0); - }); - }); - }); - }); - - describe('as a Group owner', () => { - const group = buildGroup({ - relationship: buildGroupRelationship({ - role: GroupRoles.OWNER, - member: true, - }), - }); - - describe('when the user has role of "user"', () => { - const accountId = '4'; - const groupMember = buildGroupMember({}, { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - describe('when "canPromoteToAdmin is true', () => { - it('should render dropdown with correct Owner actions', async () => { - const user = userEvent.setup(); - - render(); - - await waitFor(async() => { - await user.click(screen.getByTestId('icon-button')); - }); - - const dropdownMenu = screen.getByTestId('dropdown-menu'); - expect(dropdownMenu).toHaveTextContent('Assign admin role'); - expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); - expect(dropdownMenu).toHaveTextContent('Ban from group'); - }); - }); - - describe('when "canPromoteToAdmin is false', () => { - it('should prevent promoting user to Admin', async () => { - const user = userEvent.setup(); - - render(); - - await waitFor(async() => { - await user.click(screen.getByTestId('icon-button')); - await user.click(screen.getByTitle('Assign admin role')); - }); - - expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached'); - }); - }); - }); - - describe('when the user has role of "admin"', () => { - const accountId = '4'; - const groupMember = buildGroupMember( - { - role: GroupRoles.ADMIN, - }, - { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }, - ); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render dropdown with correct Owner actions', async () => { - const user = userEvent.setup(); - - render(); - - await waitFor(async() => { - await user.click(screen.getByTestId('icon-button')); - }); - - const dropdownMenu = screen.getByTestId('dropdown-menu'); - expect(dropdownMenu).toHaveTextContent('Remove admin role'); - expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); - expect(dropdownMenu).toHaveTextContent('Ban from group'); - }); - }); - }); - - describe('as a Group admin', () => { - const group = buildGroup({ - relationship: buildGroupRelationship({ - role: GroupRoles.ADMIN, - member: true, - }), - }); - - describe('when the user has role of "user"', () => { - const accountId = '4'; - const groupMember = buildGroupMember({}, { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should render dropdown with correct Admin actions', async () => { - const user = userEvent.setup(); - - render(); - - await waitFor(async() => { - await user.click(screen.getByTestId('icon-button')); - }); - - const dropdownMenu = screen.getByTestId('dropdown-menu'); - expect(dropdownMenu).not.toHaveTextContent('Assign admin role'); - expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); - expect(dropdownMenu).toHaveTextContent('Ban from group'); - }); - }); - - describe('when the user has role of "admin"', () => { - const accountId = '4'; - const groupMember = buildGroupMember( - { - role: GroupRoles.ADMIN, - }, - { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }, - ); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should not render the dropdown', async () => { - render(); - - await waitFor(async() => { - expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - }); - }); - }); - - describe('when the user has role of "owner"', () => { - const accountId = '4'; - const groupMember = buildGroupMember( - { - role: GroupRoles.OWNER, - }, - { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }, - ); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should not render the dropdown', async () => { - render(); - - await waitFor(async() => { - expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - }); - }); - }); - }); - - describe('as a Group user', () => { - const group = buildGroup({ - relationship: buildGroupRelationship({ - role: GroupRoles.USER, - member: true, - }), - }); - const accountId = '4'; - const groupMember = buildGroupMember({}, { - id: accountId, - display_name: 'tiger woods', - username: 'tiger', - }); - - beforeEach(() => { - __stub((mock) => { - mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); - }); - }); - - it('should not render the dropdown', async () => { - render(); - - await waitFor(async() => { - expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); - }); - }); - }); -}); \ No newline at end of file diff --git a/src/features/group/group-membership-requests.tsx b/src/features/group/group-membership-requests.tsx index 39a51c339..7887966f4 100644 --- a/src/features/group/group-membership-requests.tsx +++ b/src/features/group/group-membership-requests.tsx @@ -1,7 +1,7 @@ -import { AxiosError } from 'axios'; import { useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks/index.ts'; import Account from 'soapbox/components/account.tsx'; import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons.tsx'; @@ -85,14 +85,17 @@ const GroupMembershipRequests: React.FC = ({ params }) async function handleAuthorize(account: AccountEntity) { return authorize(account.id) .then(() => Promise.resolve()) - .catch((error: AxiosError) => { + .catch(async (error: unknown) => { refetch(); - let message = intl.formatMessage(messages.authorizeFail, { name: account.username }); - if (error.response?.status === 409) { - message = (error.response?.data as any).error; + const message = intl.formatMessage(messages.authorizeFail, { name: account.username }); + + if (error instanceof HTTPError && error.response.status === 409) { + const data = await error.response.error(); + toast.error(data?.error || message); + } else { + toast.error(message); } - toast.error(message); return Promise.reject(); }); @@ -101,14 +104,17 @@ const GroupMembershipRequests: React.FC = ({ params }) async function handleReject(account: AccountEntity) { return reject(account.id) .then(() => Promise.resolve()) - .catch((error: AxiosError) => { + .catch(async (error: unknown) => { refetch(); - let message = intl.formatMessage(messages.rejectFail, { name: account.username }); - if (error.response?.status === 409) { - message = (error.response?.data as any).error; + const message = intl.formatMessage(messages.rejectFail, { name: account.username }); + + if (error instanceof HTTPError && error.response.status === 409) { + const data = await error.response.error(); + toast.error(data?.error || message); + } else { + toast.error(message); } - toast.error(message); return Promise.reject(); }); diff --git a/src/features/groups/components/discover/search/search.test.tsx b/src/features/groups/components/discover/search/search.test.tsx deleted file mode 100644 index 44649c2af..000000000 --- a/src/features/groups/components/discover/search/search.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { __stub } from 'soapbox/api/index.ts'; -import { buildGroup } from 'soapbox/jest/factory.ts'; -import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx'; -import { instanceV1Schema } from 'soapbox/schemas/instance.ts'; - -import Search from './search.tsx'; - -const store = { - instance: instanceV1Schema.parse({ - version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', - }), -}; - -const renderApp = (children: React.ReactElement) => render(children, undefined, store); - -describe('', () => { - describe('with no results', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups/search').reply(200, []); - }); - }); - - it('should render the blankslate', async () => { - renderApp(); - - await waitFor(() => { - expect(screen.getByTestId('no-results')).toBeInTheDocument(); - }); - }); - }); - - describe('with results', () => { - beforeEach(() => { - __stub((mock) => { - mock.onGet('/api/v1/groups/search').reply(200, [ - buildGroup({ - display_name: 'Group', - id: '1', - }), - ]); - }); - }); - - it('should render the results', async () => { - renderApp(); - - await waitFor(() => { - expect(screen.getByTestId('results')).toBeInTheDocument(); - }); - }); - }); - - describe('before starting a search', () => { - it('should render the RecentSearches component', () => { - renderApp(); - - expect(screen.getByTestId('recent-searches')).toBeInTheDocument(); - }); - }); -}); \ No newline at end of file diff --git a/src/features/status/components/thread.tsx b/src/features/status/components/thread.tsx index bd0734098..719e3d90a 100644 --- a/src/features/status/components/thread.tsx +++ b/src/features/status/components/thread.tsx @@ -79,7 +79,7 @@ interface IThread { withMedia?: boolean; useWindowScroll?: boolean; itemClassName?: string; - next: string | undefined; + next?: string | null; handleLoadMore: () => void; } diff --git a/src/features/status/index.tsx b/src/features/status/index.tsx index 8ca22eb27..25a675700 100644 --- a/src/features/status/index.tsx +++ b/src/features/status/index.tsx @@ -57,7 +57,7 @@ const StatusDetails: React.FC = (props) => { const status = useAppSelector((state) => getStatus(state, { id: props.params.statusId })); const [isLoaded, setIsLoaded] = useState(!!status); - const [next, setNext] = useState(); + const [next, setNext] = useState(null); /** Fetch the status (and context) from the API. */ const fetchData = async () => { diff --git a/src/features/test-timeline/index.tsx b/src/features/test-timeline/index.tsx index 066a5174c..afcdea1f9 100644 --- a/src/features/test-timeline/index.tsx +++ b/src/features/test-timeline/index.tsx @@ -36,7 +36,7 @@ const TestTimeline: React.FC = () => { useEffect(() => { dispatch(importFetchedStatuses(MOCK_STATUSES)); - dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, undefined, undefined, false, false, false)); + dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, null, false, false, false)); }, []); return ( diff --git a/src/features/ui/components/modals/media-modal.tsx b/src/features/ui/components/modals/media-modal.tsx index 62573f1b3..0654a444b 100644 --- a/src/features/ui/components/modals/media-modal.tsx +++ b/src/features/ui/components/modals/media-modal.tsx @@ -76,7 +76,7 @@ const MediaModal: React.FC = (props) => { const actualStatus = status ? getActualStatus(status) : undefined; const [isLoaded, setIsLoaded] = useState(!!status); - const [next, setNext] = useState(); + const [next, setNext] = useState(null); const [index, setIndex] = useState(null); const [navigationHidden, setNavigationHidden] = useState(false); const [isFullScreen, setIsFullScreen] = useState(!status); diff --git a/src/features/ui/components/modals/onboarding-flow-modal/steps/avatar-step.tsx b/src/features/ui/components/modals/onboarding-flow-modal/steps/avatar-step.tsx index c97e77ee2..2d8500ee1 100644 --- a/src/features/ui/components/modals/onboarding-flow-modal/steps/avatar-step.tsx +++ b/src/features/ui/components/modals/onboarding-flow-modal/steps/avatar-step.tsx @@ -5,6 +5,7 @@ import { useRef, useState } from 'react'; import { FormattedMessage, defineMessages } from 'react-intl'; import { patchMe } from 'soapbox/actions/me.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Avatar from 'soapbox/components/ui/avatar.tsx'; import Button from 'soapbox/components/ui/button.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; @@ -18,8 +19,6 @@ import toast from 'soapbox/toast.tsx'; import { isDefaultAvatar } from 'soapbox/utils/accounts.ts'; import resizeImage from 'soapbox/utils/resize-image.ts'; -import type { AxiosError } from 'axios'; - const closeIcon = xIcon; const messages = defineMessages({ @@ -64,13 +63,13 @@ const AvatarSelectionModal: React.FC = ({ onClose, onNext setDisabled(false); setSubmitting(false); onNext(); - }).catch((error: AxiosError) => { + }).catch((error) => { setSubmitting(false); setDisabled(false); setSelectedFile(null); - if (error.response?.status === 422) { - toast.error((error.response.data as any).error.replace('Validation failed: ', '')); + if (error instanceof HTTPError && error.response.status === 422) { + toast.showAlertForError(error); } else { toast.error(messages.error); } diff --git a/src/features/ui/components/modals/onboarding-flow-modal/steps/bio-step.tsx b/src/features/ui/components/modals/onboarding-flow-modal/steps/bio-step.tsx index 0f38ebb83..90044bbc9 100644 --- a/src/features/ui/components/modals/onboarding-flow-modal/steps/bio-step.tsx +++ b/src/features/ui/components/modals/onboarding-flow-modal/steps/bio-step.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { patchMe } from 'soapbox/actions/me.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Button from 'soapbox/components/ui/button.tsx'; import FormGroup from 'soapbox/components/ui/form-group.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; @@ -13,8 +14,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import toast from 'soapbox/toast.tsx'; -import type { AxiosError } from 'axios'; - const messages = defineMessages({ bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' }, error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' }, @@ -45,11 +44,14 @@ const BioStep: React.FC = ({ onClose, onNext }) => { .then(() => { setSubmitting(false); onNext(); - }).catch((error: AxiosError) => { + }).catch(async (error) => { setSubmitting(false); - if (error.response?.status === 422) { - setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]); + if (error instanceof HTTPError && error.response.status === 422) { + const data = await error.response.error(); + if (data) { + setErrors([data.error]); + } } else { toast.error(messages.error); } diff --git a/src/features/ui/components/modals/onboarding-flow-modal/steps/cover-photo-selection-step.tsx b/src/features/ui/components/modals/onboarding-flow-modal/steps/cover-photo-selection-step.tsx index 3a5968c20..b034b746a 100644 --- a/src/features/ui/components/modals/onboarding-flow-modal/steps/cover-photo-selection-step.tsx +++ b/src/features/ui/components/modals/onboarding-flow-modal/steps/cover-photo-selection-step.tsx @@ -5,6 +5,7 @@ import { useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { patchMe } from 'soapbox/actions/me.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import StillImage from 'soapbox/components/still-image.tsx'; import Avatar from 'soapbox/components/ui/avatar.tsx'; import Button from 'soapbox/components/ui/button.tsx'; @@ -19,8 +20,6 @@ import toast from 'soapbox/toast.tsx'; import { isDefaultHeader } from 'soapbox/utils/accounts.ts'; import resizeImage from 'soapbox/utils/resize-image.ts'; -import type { AxiosError } from 'axios'; - const closeIcon = xIcon; const messages = defineMessages({ @@ -68,13 +67,18 @@ const CoverPhotoSelectionModal: React.FC = ({ onClose setDisabled(false); setSubmitting(false); onNext(); - }).catch((error: AxiosError) => { + }).catch(async (error) => { setSubmitting(false); setDisabled(false); setSelectedFile(null); - if (error.response?.status === 422) { - toast.error((error.response.data as any).error.replace('Validation failed: ', '')); + if (error instanceof HTTPError && error.response?.status === 422) { + const data = await error.response.error(); + if (data) { + toast.error(data.error); + } else { + toast.error(messages.error); + } } else { toast.error(messages.error); } diff --git a/src/features/ui/components/modals/onboarding-flow-modal/steps/display-name-step.tsx b/src/features/ui/components/modals/onboarding-flow-modal/steps/display-name-step.tsx index 98ccc0fb3..0bf9b91bb 100644 --- a/src/features/ui/components/modals/onboarding-flow-modal/steps/display-name-step.tsx +++ b/src/features/ui/components/modals/onboarding-flow-modal/steps/display-name-step.tsx @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { patchMe } from 'soapbox/actions/me.ts'; +import { HTTPError } from 'soapbox/api/HTTPError.ts'; import Button from 'soapbox/components/ui/button.tsx'; import FormGroup from 'soapbox/components/ui/form-group.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; @@ -13,8 +14,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import toast from 'soapbox/toast.tsx'; -import type { AxiosError } from 'axios'; - const closeIcon = xIcon; const messages = defineMessages({ @@ -56,11 +55,14 @@ const DisplayNameStep: React.FC = ({ onClose, onNext }) => { .then(() => { setSubmitting(false); onNext(); - }).catch((error: AxiosError) => { + }).catch(async (error) => { setSubmitting(false); - if (error.response?.status === 422) { - setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]); + if (error instanceof HTTPError && error.response?.status === 422) { + const data = await error.response.error(); + if (data) { + setErrors([data.error]); + } } else { toast.error(messages.error); } diff --git a/src/features/ui/components/navbar.tsx b/src/features/ui/components/navbar.tsx index 249668384..dad17150b 100644 --- a/src/features/ui/components/navbar.tsx +++ b/src/features/ui/components/navbar.tsx @@ -4,7 +4,7 @@ import { useRef, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link, Redirect } from 'react-router-dom'; -import { logIn, verifyCredentials } from 'soapbox/actions/auth.ts'; +import { logIn, MfaRequiredError, verifyCredentials } from 'soapbox/actions/auth.ts'; import { fetchInstance } from 'soapbox/actions/instance.ts'; import { openModal } from 'soapbox/actions/modals.ts'; import { openSidebar } from 'soapbox/actions/sidebar.ts'; @@ -28,8 +28,6 @@ import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications import ProfileDropdown from './profile-dropdown.tsx'; -import type { AxiosError } from 'axios'; - const messages = defineMessages({ login: { id: 'navbar.login.action', defaultMessage: 'Log in' }, username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' }, @@ -52,7 +50,7 @@ const Navbar = () => { const [isLoading, setLoading] = useState(false); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [mfaToken, setMfaToken] = useState(false); + const [mfaToken, setMfaToken] = useState(); const onOpenSidebar = () => dispatch(openSidebar()); @@ -74,17 +72,18 @@ const Navbar = () => { .then(() => dispatch(fetchInstance())) ); }) - .catch((error: AxiosError) => { + .catch((error: unknown) => { setLoading(false); - const data: any = error.response?.data; - if (data?.error === 'mfa_required') { - setMfaToken(data.mfa_token); + if (error instanceof MfaRequiredError) { + setMfaToken(error.token); } }); }; - if (mfaToken) return ; + if (mfaToken) { + return ; + } return (