D app/javascript/mastodon/actions/account_notes.js => app/javascript/mastodon/actions/account_notes.js +0 -37
@@ 1,37 0,0 @@
-import api from '../api';
-
-export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
-export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
-export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
-
-export function submitAccountNote(id, value) {
- return (dispatch, getState) => {
- dispatch(submitAccountNoteRequest());
-
- api(getState).post(`/api/v1/accounts/${id}/note`, {
- comment: value,
- }).then(response => {
- dispatch(submitAccountNoteSuccess(response.data));
- }).catch(error => dispatch(submitAccountNoteFail(error)));
- };
-}
-
-export function submitAccountNoteRequest() {
- return {
- type: ACCOUNT_NOTE_SUBMIT_REQUEST,
- };
-}
-
-export function submitAccountNoteSuccess(relationship) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
- relationship,
- };
-}
-
-export function submitAccountNoteFail(error) {
- return {
- type: ACCOUNT_NOTE_SUBMIT_FAIL,
- error,
- };
-}
A app/javascript/mastodon/actions/account_notes.ts => app/javascript/mastodon/actions/account_notes.ts +18 -0
@@ 0,0 1,18 @@
+import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
+
+import api from '../api';
+
+export const submitAccountNote = createAppAsyncThunk(
+ 'account_note/submit',
+ async (args: { id: string; value: string }, { getState }) => {
+ // TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
+ const response = await api(getState).post<unknown>(
+ `/api/v1/accounts/${args.id}/note`,
+ {
+ comment: args.value,
+ },
+ );
+
+ return { relationship: response.data };
+ },
+);
R app/javascript/mastodon/api.js => app/javascript/mastodon/api.ts +18 -31
@@ 1,16 1,12 @@
-// @ts-check
-
+import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready';
+import type { GetState } from './store';
-/**
- * @param {import('axios').AxiosResponse} response
- * @returns {LinkHeader}
- */
-export const getLinks = response => {
- const value = response.headers.link;
+export const getLinks = (response: AxiosResponse) => {
+ const value = response.headers.link as string | undefined;
if (!value) {
return new LinkHeader();
@@ 19,44 15,35 @@ export const getLinks = response => {
return LinkHeader.parse(value);
};
-/** @type {import('axios').RawAxiosRequestHeaders} */
-const csrfHeader = {};
+const csrfHeader: RawAxiosRequestHeaders = {};
-/**
- * @returns {void}
- */
const setCSRFHeader = () => {
- /** @type {HTMLMetaElement | null} */
- const csrfToken = document.querySelector('meta[name=csrf-token]');
+ const csrfToken = document.querySelector<HTMLMetaElement>(
+ 'meta[name=csrf-token]',
+ );
if (csrfToken) {
csrfHeader['X-CSRF-Token'] = csrfToken.content;
}
};
-ready(setCSRFHeader);
+void ready(setCSRFHeader);
-/**
- * @param {() => import('immutable').Map<string,any>} getState
- * @returns {import('axios').RawAxiosRequestHeaders}
- */
-const authorizationHeaderFromState = getState => {
- const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
+const authorizationHeaderFromState = (getState?: GetState) => {
+ const accessToken =
+ getState && (getState().meta.get('access_token', '') as string);
if (!accessToken) {
return {};
}
return {
- 'Authorization': `Bearer ${accessToken}`,
- };
+ Authorization: `Bearer ${accessToken}`,
+ } as RawAxiosRequestHeaders;
};
-/**
- * @param {() => import('immutable').Map<string,any>} getState
- * @returns {import('axios').AxiosInstance}
- */
-export default function api(getState) {
+// eslint-disable-next-line import/no-default-export
+export default function api(getState: GetState) {
return axios.create({
headers: {
...csrfHeader,
@@ 64,9 51,9 @@ export default function api(getState) {
},
transformResponse: [
- function (data) {
+ function (data: unknown) {
try {
- return JSON.parse(data);
+ return JSON.parse(data as string) as unknown;
} catch {
return data;
}
M app/javascript/mastodon/features/account/containers/account_note_container.js => app/javascript/mastodon/features/account/containers/account_note_container.js +1 -1
@@ 11,7 11,7 @@ const mapStateToProps = (state, { account }) => ({
const mapDispatchToProps = (dispatch, { account }) => ({
onSave (value) {
- dispatch(submitAccountNote(account.get('id'), value));
+ dispatch(submitAccountNote({ id: account.get('id'), value}));
},
});
M app/javascript/mastodon/reducers/relationships.js => app/javascript/mastodon/reducers/relationships.js +3 -3
@@ 1,7 1,7 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
- ACCOUNT_NOTE_SUBMIT_SUCCESS,
+ submitAccountNote,
} from '../actions/account_notes';
import {
ACCOUNT_FOLLOW_SUCCESS,
@@ 73,10 73,10 @@ export default function relationships(state = initialState, action) {
case ACCOUNT_UNMUTE_SUCCESS:
case ACCOUNT_PIN_SUCCESS:
case ACCOUNT_UNPIN_SUCCESS:
- case ACCOUNT_NOTE_SUBMIT_SUCCESS:
- return normalizeRelationship(state, action.relationship);
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, action.relationships);
+ case submitAccountNote.fulfilled:
+ return normalizeRelationship(state, action.payload.relationship);
case DOMAIN_BLOCK_SUCCESS:
return setDomainBlocking(state, action.accounts, true);
case DOMAIN_UNBLOCK_SUCCESS:
M app/javascript/mastodon/store/index.ts => app/javascript/mastodon/store/index.ts +8 -45
@@ 1,45 1,8 @@
-import type { TypedUseSelectorHook } from 'react-redux';
-import { useDispatch, useSelector } from 'react-redux';
-
-import { configureStore } from '@reduxjs/toolkit';
-
-import { rootReducer } from '../reducers';
-
-import { errorsMiddleware } from './middlewares/errors';
-import { loadingBarMiddleware } from './middlewares/loading_bar';
-import { soundsMiddleware } from './middlewares/sounds';
-
-export const store = configureStore({
- reducer: rootReducer,
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
- // In development, Redux Toolkit enables 2 default middlewares to detect
- // common issues with states. Unfortunately, our use of ImmutableJS for state
- // triggers both, so lets disable them until our state is fully refactored
-
- // https://redux-toolkit.js.org/api/serializabilityMiddleware
- // This checks recursively that every values in the state are serializable in JSON
- // Which is not the case, as we use ImmutableJS structures, but also File objects
- serializableCheck: false,
-
- // https://redux-toolkit.js.org/api/immutabilityMiddleware
- // This checks recursively if every value in the state is immutable (ie, a JS primitive type)
- // But this is not the case, as our Root State is an ImmutableJS map, which is an object
- immutableCheck: false,
- })
- .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;
+export { store } from './store';
+export type { GetState, AppDispatch, RootState } from './store';
+
+export {
+ createAppAsyncThunk,
+ useAppDispatch,
+ useAppSelector,
+} from './typed_functions';
A app/javascript/mastodon/store/store.ts => app/javascript/mastodon/store/store.ts +40 -0
@@ 0,0 1,40 @@
+import { configureStore } from '@reduxjs/toolkit';
+
+import { rootReducer } from '../reducers';
+
+import { errorsMiddleware } from './middlewares/errors';
+import { loadingBarMiddleware } from './middlewares/loading_bar';
+import { soundsMiddleware } from './middlewares/sounds';
+
+export const store = configureStore({
+ reducer: rootReducer,
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ // In development, Redux Toolkit enables 2 default middlewares to detect
+ // common issues with states. Unfortunately, our use of ImmutableJS for state
+ // triggers both, so lets disable them until our state is fully refactored
+
+ // https://redux-toolkit.js.org/api/serializabilityMiddleware
+ // This checks recursively that every values in the state are serializable in JSON
+ // Which is not the case, as we use ImmutableJS structures, but also File objects
+ serializableCheck: false,
+
+ // https://redux-toolkit.js.org/api/immutabilityMiddleware
+ // This checks recursively if every value in the state is immutable (ie, a JS primitive type)
+ // But this is not the case, as our Root State is an ImmutableJS map, which is an object
+ immutableCheck: false,
+ })
+ .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 type GetState = typeof store.getState;
A app/javascript/mastodon/store/typed_functions.ts => app/javascript/mastodon/store/typed_functions.ts +16 -0
@@ 0,0 1,16 @@
+import type { TypedUseSelectorHook } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+import type { AppDispatch, RootState } from './store';
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+
+export const createAppAsyncThunk = createAsyncThunk.withTypes<{
+ state: RootState;
+ dispatch: AppDispatch;
+ rejectValue: string;
+ extra: { s: string; n: number };
+}>();