~cytrogen/masto-fe

bd06c13204b13818cb2d7695d9af25fe813fcdb5 — Renaud Chaput 2 years ago 7730083
Convert `actions/account_notes` into Typescript (#26601)

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 };
}>();