diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index a4c5f3cb49..9213c5614e 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import { store } from '../store/configureStore'; +import { store } from '../store'; import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 256ea8e2d9..9c6c9e5920 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; -import { store } from 'mastodon/store/configureStore'; +import { store } from 'mastodon/store'; import UI from 'mastodon/features/ui'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { hydrateStore } from 'mastodon/actions/store'; diff --git a/app/javascript/mastodon/main.jsx b/app/javascript/mastodon/main.jsx index d8654adedb..c5960477db 100644 --- a/app/javascript/mastodon/main.jsx +++ b/app/javascript/mastodon/main.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { setupBrowserNotifications } from 'mastodon/actions/notifications'; import Mastodon from 'mastodon/containers/mastodon'; -import { store } from 'mastodon/store/configureStore'; +import { store } from 'mastodon/store'; import { me } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; import * as perf from 'mastodon/performance'; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.ts similarity index 97% rename from app/javascript/mastodon/reducers/index.js rename to app/javascript/mastodon/reducers/index.ts index 4d705f0412..518d8cd792 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.ts @@ -87,4 +87,6 @@ const reducers = { followed_tags, }; -export default combineReducers(reducers); +const rootReducer = combineReducers(reducers); + +export { rootReducer }; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js deleted file mode 100644 index cb17dd9ce8..0000000000 --- a/app/javascript/mastodon/store/configureStore.js +++ /dev/null @@ -1,16 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit'; -import thunk from 'redux-thunk'; -import appReducer from '../reducers'; -import loadingBarMiddleware from '../middleware/loading_bar'; -import errorsMiddleware from '../middleware/errors'; -import soundsMiddleware from '../middleware/sounds'; - -export const store = configureStore({ - reducer: appReducer, - middleware: [ - thunk, - loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), - errorsMiddleware(), - soundsMiddleware(), - ], -}); diff --git a/app/javascript/mastodon/store/index.ts b/app/javascript/mastodon/store/index.ts new file mode 100644 index 0000000000..822c01aa90 --- /dev/null +++ b/app/javascript/mastodon/store/index.ts @@ -0,0 +1,23 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { rootReducer } from '../reducers'; +import { loadingBarMiddleware } from './middlewares/loading_bar'; +import { errorsMiddleware } from './middlewares/errors'; +import { soundsMiddleware } from './middlewares/sounds'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; + +export const store = configureStore({ + reducer: rootReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware().concat( + loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] })) + .concat(errorsMiddleware) + .concat(soundsMiddleware()), +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch + +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/app/javascript/mastodon/middleware/errors.js b/app/javascript/mastodon/store/middlewares/errors.ts similarity index 55% rename from app/javascript/mastodon/middleware/errors.js rename to app/javascript/mastodon/store/middlewares/errors.ts index 708df6bb8d..b135fa2ee8 100644 --- a/app/javascript/mastodon/middleware/errors.js +++ b/app/javascript/mastodon/store/middlewares/errors.ts @@ -1,9 +1,11 @@ -import { showAlertForError } from '../actions/alerts'; +import { Middleware } from 'redux'; +import { showAlertForError } from '../../actions/alerts'; +import { RootState } from '..'; const defaultFailSuffix = 'FAIL'; -export default function errorsMiddleware() { - return ({ dispatch }) => next => action => { +export const errorsMiddleware: Middleware, RootState> = + ({ dispatch }) => next => action => { if (action.type && !action.skipAlert) { const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); @@ -14,4 +16,3 @@ export default function errorsMiddleware() { return next(action); }; -} diff --git a/app/javascript/mastodon/middleware/loading_bar.js b/app/javascript/mastodon/store/middlewares/loading_bar.ts similarity index 68% rename from app/javascript/mastodon/middleware/loading_bar.js rename to app/javascript/mastodon/store/middlewares/loading_bar.ts index da8cc4c7d3..e860b31b6f 100644 --- a/app/javascript/mastodon/middleware/loading_bar.js +++ b/app/javascript/mastodon/store/middlewares/loading_bar.ts @@ -1,8 +1,14 @@ import { showLoading, hideLoading } from 'react-redux-loading-bar'; +import { Middleware } from 'redux'; +import { RootState } from '..'; -const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED']; +interface Config { + promiseTypeSuffixes?: string[] +} -export default function loadingBarMiddleware(config = {}) { +const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = ['PENDING', 'FULFILLED', 'REJECTED']; + +export const loadingBarMiddleware = (config: Config = {}): Middleware, RootState> => { const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes; return ({ dispatch }) => next => (action) => { @@ -22,4 +28,4 @@ export default function loadingBarMiddleware(config = {}) { return next(action); }; -} +}; diff --git a/app/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/store/middlewares/sounds.ts similarity index 54% rename from app/javascript/mastodon/middleware/sounds.js rename to app/javascript/mastodon/store/middlewares/sounds.ts index 7f23889836..c9d51f857f 100644 --- a/app/javascript/mastodon/middleware/sounds.js +++ b/app/javascript/mastodon/store/middlewares/sounds.ts @@ -1,4 +1,12 @@ -const createAudio = sources => { +import { Middleware, AnyAction } from 'redux'; +import { RootState } from '..'; + +interface AudioSource { + src: string + type: string +} + +const createAudio = (sources: AudioSource[]) => { const audio = new Audio(); sources.forEach(({ type, src }) => { const source = document.createElement('source'); @@ -9,7 +17,7 @@ const createAudio = sources => { return audio; }; -const play = audio => { +const play = (audio: HTMLAudioElement) => { if (!audio.paused) { audio.pause(); if (typeof audio.fastSeek === 'function') { @@ -22,8 +30,8 @@ const play = audio => { audio.play(); }; -export default function soundsMiddleware() { - const soundCache = { +export const soundsMiddleware = (): Middleware, RootState> => { + const soundCache: {[key: string]: HTMLAudioElement} = { boop: createAudio([ { src: '/sounds/boop.ogg', @@ -36,11 +44,13 @@ export default function soundsMiddleware() { ]), }; - return () => next => action => { - if (action.meta && action.meta.sound && soundCache[action.meta.sound]) { - play(soundCache[action.meta.sound]); + return () => next => (action: AnyAction) => { + const sound = action?.meta?.sound; + + if (sound && soundCache[sound]) { + play(soundCache[sound]); } return next(action); }; -} +};