M app/javascript/mastodon/containers/compose_container.jsx => app/javascript/mastodon/containers/compose_container.jsx +1 -1
@@ 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';
M app/javascript/mastodon/containers/mastodon.jsx => app/javascript/mastodon/containers/mastodon.jsx +1 -1
@@ 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';
M app/javascript/mastodon/main.jsx => app/javascript/mastodon/main.jsx +1 -1
@@ 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';
R app/javascript/mastodon/reducers/index.js => app/javascript/mastodon/reducers/index.ts +3 -1
@@ 87,4 87,6 @@ const reducers = {
followed_tags,
};
-export default combineReducers(reducers);
+const rootReducer = combineReducers(reducers);
+
+export { rootReducer };
D app/javascript/mastodon/store/configureStore.js => app/javascript/mastodon/store/configureStore.js +0 -16
@@ 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(),
- ],
-});
A app/javascript/mastodon/store/index.ts => app/javascript/mastodon/store/index.ts +23 -0
@@ 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<typeof rootReducer>
+// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
+export type AppDispatch = typeof store.dispatch
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
R app/javascript/mastodon/middleware/errors.js => app/javascript/mastodon/store/middlewares/errors.ts +5 -4
@@ 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<Record<string, never>, 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);
};
-}
R app/javascript/mastodon/middleware/loading_bar.js => app/javascript/mastodon/store/middlewares/loading_bar.ts +9 -3
@@ 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[]
+}
+
+const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = ['PENDING', 'FULFILLED', 'REJECTED'];
-export default function loadingBarMiddleware(config = {}) {
+export const loadingBarMiddleware = (config: Config = {}): Middleware<Record<string, never>, RootState> => {
const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
return ({ dispatch }) => next => (action) => {
@@ 22,4 28,4 @@ export default function loadingBarMiddleware(config = {}) {
return next(action);
};
-}
+};
R app/javascript/mastodon/middleware/sounds.js => app/javascript/mastodon/store/middlewares/sounds.ts +18 -8
@@ 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<Record<string, never>, 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);
};
-}
+};