diff --git a/app/soapbox/actions/instance.ts b/app/soapbox/actions/instance.ts index 1c826e3d2..0d6d6c2e4 100644 --- a/app/soapbox/actions/instance.ts +++ b/app/soapbox/actions/instance.ts @@ -1,30 +1,19 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; import { get } from 'lodash'; import KVStore from 'soapbox/storage/kv_store'; -import { AppDispatch, RootState } from 'soapbox/store'; +import { RootState } from 'soapbox/store'; import { getAuthUserUrl } from 'soapbox/utils/auth'; import { parseVersion } from 'soapbox/utils/features'; import api from '../api'; -export const INSTANCE_FETCH_REQUEST = 'INSTANCE_FETCH_REQUEST'; -export const INSTANCE_FETCH_SUCCESS = 'INSTANCE_FETCH_SUCCESS'; -export const INSTANCE_FETCH_FAIL = 'INSTANCE_FETCH_FAIL'; - -export const INSTANCE_REMEMBER_REQUEST = 'INSTANCE_REMEMBER_REQUEST'; -export const INSTANCE_REMEMBER_SUCCESS = 'INSTANCE_REMEMBER_SUCCESS'; -export const INSTANCE_REMEMBER_FAIL = 'INSTANCE_REMEMBER_FAIL'; - -export const NODEINFO_FETCH_REQUEST = 'NODEINFO_FETCH_REQUEST'; -export const NODEINFO_FETCH_SUCCESS = 'NODEINFO_FETCH_SUCCESS'; -export const NODEINFO_FETCH_FAIL = 'NODEINFO_FETCH_FAIL'; - const getMeUrl = (state: RootState) => { const me = state.me; return state.accounts.getIn([me, 'url']); }; -// Figure out the appropriate instance to fetch depending on the state +/** Figure out the appropriate instance to fetch depending on the state */ export const getHost = (state: RootState) => { const accountUrl = getMeUrl(state) || getAuthUserUrl(state); @@ -35,60 +24,45 @@ export const getHost = (state: RootState) => { } }; -export function rememberInstance(host: string) { - return (dispatch: AppDispatch, _getState: () => RootState) => { - dispatch({ type: INSTANCE_REMEMBER_REQUEST, host }); - return KVStore.getItemOrError(`instance:${host}`).then((instance: Record) => { - dispatch({ type: INSTANCE_REMEMBER_SUCCESS, host, instance }); - return instance; - }).catch((error: Error) => { - dispatch({ type: INSTANCE_REMEMBER_FAIL, host, error, skipAlert: true }); - }); - }; -} +export const rememberInstance = createAsyncThunk( + 'instance/remember', + async(host: string) => { + return await KVStore.getItemOrError(`instance:${host}`); + }, +); -// We may need to fetch nodeinfo on Pleroma < 2.1 +/** We may need to fetch nodeinfo on Pleroma < 2.1 */ const needsNodeinfo = (instance: Record): boolean => { const v = parseVersion(get(instance, 'version')); return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']); }; -export function fetchInstance() { - return (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: INSTANCE_FETCH_REQUEST }); - return api(getState).get('/api/v1/instance').then(({ data: instance }: { data: Record }) => { - dispatch({ type: INSTANCE_FETCH_SUCCESS, instance }); - if (needsNodeinfo(instance)) { - // @ts-ignore: ??? - dispatch(fetchNodeinfo()); // Pleroma < 2.1 backwards compatibility - } - }).catch(error => { - console.error(error); - dispatch({ type: INSTANCE_FETCH_FAIL, error, skipAlert: true }); - }); - }; -} +export const fetchInstance = createAsyncThunk( + 'instance/fetch', + async(_arg, { dispatch, getState }) => { + const { data: instance } = await api(getState).get('/api/v1/instance'); + if (needsNodeinfo(instance)) { + dispatch(fetchNodeinfo()); + } + return instance; + }, +); -// Tries to remember the instance from browser storage before fetching it -export function loadInstance() { - return (dispatch: AppDispatch, getState: () => RootState) => { +/** Tries to remember the instance from browser storage before fetching it */ +export const loadInstance = createAsyncThunk( + 'instance/load', + async(_arg, { dispatch, getState }) => { const host = getHost(getState()); + await Promise.all([ + dispatch(rememberInstance(host || '')), + dispatch(fetchInstance()), + ]); + }, +); - // @ts-ignore: ??? - return dispatch(rememberInstance(host)).finally(() => { - // @ts-ignore: ??? - return dispatch(fetchInstance()); - }); - }; -} - -export function fetchNodeinfo() { - return (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: NODEINFO_FETCH_REQUEST }); - return api(getState).get('/nodeinfo/2.1.json').then(({ data: nodeinfo }) => { - return dispatch({ type: NODEINFO_FETCH_SUCCESS, nodeinfo }); - }).catch((error: Error) => { - return dispatch({ type: NODEINFO_FETCH_FAIL, error, skipAlert: true }); - }); - }; -} +export const fetchNodeinfo = createAsyncThunk( + 'nodeinfo/fetch', + async(_arg, { getState }) => { + return await api(getState).get('/nodeinfo/2.1.json'); + }, +); diff --git a/app/soapbox/features/landing_page/__tests__/landing_page.test.tsx b/app/soapbox/features/landing_page/__tests__/landing_page.test.tsx index ce0ce778c..7ec1010fa 100644 --- a/app/soapbox/features/landing_page/__tests__/landing_page.test.tsx +++ b/app/soapbox/features/landing_page/__tests__/landing_page.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import LandingPage from '..'; -import { INSTANCE_REMEMBER_SUCCESS } from '../../../actions/instance'; +import { rememberInstance } from '../../../actions/instance'; import { PEPE_FETCH_INSTANCE_SUCCESS } from '../../../actions/verification'; import { render, screen, rootReducer, applyActions } from '../../../jest/test-helpers'; @@ -9,8 +9,8 @@ describe('', () => { it('renders a RegistrationForm for an open Pleroma instance', () => { const state = rootReducer(undefined, { - type: INSTANCE_REMEMBER_SUCCESS, - instance: { + type: rememberInstance.fulfilled.toString(), + payload: { version: '2.7.2 (compatible; Pleroma 2.3.0)', registrations: true, }, @@ -26,8 +26,8 @@ describe('', () => { it('renders "closed" message for a closed Pleroma instance', () => { const state = rootReducer(undefined, { - type: INSTANCE_REMEMBER_SUCCESS, - instance: { + type: rememberInstance.fulfilled.toString(), + payload: { version: '2.7.2 (compatible; Pleroma 2.3.0)', registrations: false, }, @@ -43,8 +43,8 @@ describe('', () => { it('renders Pepe flow for an open Truth Social instance', () => { const state = applyActions(undefined, [{ - type: INSTANCE_REMEMBER_SUCCESS, - instance: { + type: rememberInstance.fulfilled.toString(), + payload: { version: '3.4.1 (compatible; TruthSocial 1.0.0)', registrations: false, }, @@ -65,8 +65,8 @@ describe('', () => { it('renders "closed" message for a Truth Social instance with Pepe closed', () => { const state = applyActions(undefined, [{ - type: INSTANCE_REMEMBER_SUCCESS, - instance: { + type: rememberInstance.fulfilled.toString(), + payload: { version: '3.4.1 (compatible; TruthSocial 1.0.0)', registrations: false, }, diff --git a/app/soapbox/reducers/__tests__/instance-test.js b/app/soapbox/reducers/__tests__/instance-test.js index 0954c3878..e1e99bf07 100644 --- a/app/soapbox/reducers/__tests__/instance-test.js +++ b/app/soapbox/reducers/__tests__/instance-test.js @@ -1,7 +1,7 @@ import { Record } from 'immutable'; import { ADMIN_CONFIG_UPDATE_REQUEST } from 'soapbox/actions/admin'; -import { INSTANCE_REMEMBER_SUCCESS } from 'soapbox/actions/instance'; +import { rememberInstance } from 'soapbox/actions/instance'; import reducer from '../instance'; @@ -30,11 +30,11 @@ describe('instance reducer', () => { expect(result.toJS()).toMatchObject(expected); }); - describe('INSTANCE_REMEMBER_SUCCESS', () => { + describe('rememberInstance.fulfilled', () => { it('normalizes Pleroma instance with Mastodon configuration format', () => { const action = { - type: INSTANCE_REMEMBER_SUCCESS, - instance: require('soapbox/__fixtures__/pleroma-instance.json'), + type: rememberInstance.fulfilled.toString(), + payload: require('soapbox/__fixtures__/pleroma-instance.json'), }; const result = reducer(undefined, action); @@ -59,8 +59,8 @@ describe('instance reducer', () => { it('normalizes Mastodon instance with retained configuration', () => { const action = { - type: INSTANCE_REMEMBER_SUCCESS, - instance: require('soapbox/__fixtures__/mastodon-instance.json'), + type: rememberInstance.fulfilled.toString(), + payload: require('soapbox/__fixtures__/mastodon-instance.json'), }; const result = reducer(undefined, action); @@ -93,8 +93,8 @@ describe('instance reducer', () => { it('normalizes Mastodon 3.0.0 instance with default configuration', () => { const action = { - type: INSTANCE_REMEMBER_SUCCESS, - instance: require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'), + type: rememberInstance.fulfilled.toString(), + payload: require('soapbox/__fixtures__/mastodon-3.0.0-instance.json'), }; const result = reducer(undefined, action); diff --git a/app/soapbox/reducers/instance.ts b/app/soapbox/reducers/instance.ts index 7c832c200..16523d474 100644 --- a/app/soapbox/reducers/instance.ts +++ b/app/soapbox/reducers/instance.ts @@ -8,10 +8,9 @@ import KVStore from 'soapbox/storage/kv_store'; import { ConfigDB } from 'soapbox/utils/config_db'; import { - INSTANCE_REMEMBER_SUCCESS, - INSTANCE_FETCH_SUCCESS, - INSTANCE_FETCH_FAIL, - NODEINFO_FETCH_SUCCESS, + rememberInstance, + fetchInstance, + fetchNodeinfo, } from '../actions/instance'; const initialState = normalizeInstance(ImmutableMap()); @@ -115,15 +114,15 @@ export default function instance(state = initialState, action: AnyAction) { switch(action.type) { case PLEROMA_PRELOAD_IMPORT: return preloadImport(state, action, '/api/v1/instance'); - case INSTANCE_REMEMBER_SUCCESS: - return importInstance(state, ImmutableMap(fromJS(action.instance))); - case INSTANCE_FETCH_SUCCESS: - persistInstance(action.instance); - return importInstance(state, ImmutableMap(fromJS(action.instance))); - case INSTANCE_FETCH_FAIL: + case rememberInstance.fulfilled.toString(): + return importInstance(state, ImmutableMap(fromJS(action.payload))); + case fetchInstance.fulfilled.toString(): + persistInstance(action.payload); + return importInstance(state, ImmutableMap(fromJS(action.payload))); + case fetchInstance.rejected.toString(): return handleInstanceFetchFail(state, action.error); - case NODEINFO_FETCH_SUCCESS: - return importNodeinfo(state, ImmutableMap(fromJS(action.nodeinfo))); + case fetchNodeinfo.fulfilled.toString(): + return importNodeinfo(state, ImmutableMap(fromJS(action.payload))); case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_SUCCESS: return importConfigs(state, ImmutableList(fromJS(action.configs))); diff --git a/app/soapbox/reducers/meta.ts b/app/soapbox/reducers/meta.ts index cdcdaf580..026b050b3 100644 --- a/app/soapbox/reducers/meta.ts +++ b/app/soapbox/reducers/meta.ts @@ -2,7 +2,7 @@ import { Record as ImmutableRecord } from 'immutable'; -import { INSTANCE_FETCH_FAIL } from 'soapbox/actions/instance'; +import { fetchInstance } from 'soapbox/actions/instance'; import type { AnyAction } from 'redux'; @@ -12,7 +12,7 @@ const ReducerRecord = ImmutableRecord({ export default function meta(state = ReducerRecord(), action: AnyAction) { switch(action.type) { - case INSTANCE_FETCH_FAIL: + case fetchInstance.rejected.toString(): return state.set('instance_fetch_failed', true); default: return state; diff --git a/app/soapbox/store.ts b/app/soapbox/store.ts index 170b316c9..aba830a93 100644 --- a/app/soapbox/store.ts +++ b/app/soapbox/store.ts @@ -1,21 +1,20 @@ -import { composeWithDevTools } from '@redux-devtools/extension'; -import { createStore, applyMiddleware, AnyAction } from 'redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { AnyAction } from 'redux'; import thunk, { ThunkDispatch } from 'redux-thunk'; import errorsMiddleware from './middleware/errors'; import soundsMiddleware from './middleware/sounds'; import appReducer from './reducers'; -export const store = createStore( - appReducer, - composeWithDevTools( - applyMiddleware( - thunk, - errorsMiddleware(), - soundsMiddleware(), - ), - ), -); +export const store = configureStore({ + reducer: appReducer, + middleware: [ + thunk, + errorsMiddleware(), + soundsMiddleware(), + ], + devTools: true, +}); export type Store = typeof store; diff --git a/package.json b/package.json index 3209553ca..18cf5ba40 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@reach/rect": "^0.16.0", "@reach/tabs": "^0.16.4", "@reach/tooltip": "^0.16.2", - "@redux-devtools/extension": "^3.2.2", + "@reduxjs/toolkit": "^1.8.1", "@sentry/browser": "^6.12.0", "@sentry/react": "^6.12.0", "@sentry/tracing": "^6.12.0", diff --git a/yarn.lock b/yarn.lock index 1c4f91bf3..4a424404b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1167,7 +1167,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.0": +"@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5": version "7.17.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== @@ -1769,12 +1769,15 @@ prop-types "^15.7.2" tslib "^2.3.0" -"@redux-devtools/extension@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@redux-devtools/extension/-/extension-3.2.2.tgz#2d6da4df2c4d32a0aac54d824e46f52b1fd9fc4d" - integrity sha512-fKA2TWNzJF7wXSDwBemwcagBFudaejXCzH5hRszN3Z6B7XEJtEmGD77AjV0wliZpIZjA/fs3U7CejFMQ+ipS7A== +"@reduxjs/toolkit@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.1.tgz#94ee1981b8cf9227cda40163a04704a9544c9a9f" + integrity sha512-Q6mzbTpO9nOYRnkwpDlFOAbQnd3g7zj7CtHAZWz5SzE5lcV97Tf8f3SzOO8BoPOMYBFgfZaqTUZqgGu+a0+Fng== dependencies: - "@babel/runtime" "^7.17.0" + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" "@sentry/browser@6.12.0", "@sentry/browser@^6.12.0": version "6.12.0" @@ -5687,6 +5690,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immer@^9.0.7: + version "9.0.12" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== + immutable@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" @@ -9120,6 +9128,11 @@ redux-thunk@^2.2.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== +redux-thunk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" + integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== + redux@^4.0.0, redux@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" @@ -9134,6 +9147,13 @@ redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +redux@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" @@ -9281,6 +9301,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== +reselect@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"