From 6fb5fafd28e2981665071f78dccec6f4331bdef2 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Mon, 11 Sep 2023 16:09:22 +0200
Subject: [PATCH] [Glitch] Convert `actions/account_notes` into Typescript

Port bd06c13204b13818cb2d7695d9af25fe813fcdb5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/actions/account_notes.js  | 37 ---------
 .../flavours/glitch/actions/account_notes.ts  | 18 +++++
 app/javascript/flavours/glitch/api.js         | 75 -------------------
 app/javascript/flavours/glitch/api.ts         | 63 ++++++++++++++++
 .../containers/account_note_container.js      |  2 +-
 .../flavours/glitch/reducers/relationships.js |  6 +-
 app/javascript/flavours/glitch/store/index.ts | 51 ++-----------
 app/javascript/flavours/glitch/store/store.ts | 40 ++++++++++
 .../flavours/glitch/store/typed_functions.ts  | 16 ++++
 9 files changed, 148 insertions(+), 160 deletions(-)
 delete mode 100644 app/javascript/flavours/glitch/actions/account_notes.js
 create mode 100644 app/javascript/flavours/glitch/actions/account_notes.ts
 delete mode 100644 app/javascript/flavours/glitch/api.js
 create mode 100644 app/javascript/flavours/glitch/api.ts
 create mode 100644 app/javascript/flavours/glitch/store/store.ts
 create mode 100644 app/javascript/flavours/glitch/store/typed_functions.ts

diff --git a/app/javascript/flavours/glitch/actions/account_notes.js b/app/javascript/flavours/glitch/actions/account_notes.js
deleted file mode 100644
index 72b943300d..0000000000
--- a/app/javascript/flavours/glitch/actions/account_notes.js
+++ /dev/null
@@ -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,
-  };
-}
diff --git a/app/javascript/flavours/glitch/actions/account_notes.ts b/app/javascript/flavours/glitch/actions/account_notes.ts
new file mode 100644
index 0000000000..dbe9ee2a9f
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/account_notes.ts
@@ -0,0 +1,18 @@
+import { createAppAsyncThunk } from 'flavours/glitch/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 };
+  },
+);
diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js
deleted file mode 100644
index 948ffbc95c..0000000000
--- a/app/javascript/flavours/glitch/api.js
+++ /dev/null
@@ -1,75 +0,0 @@
-// @ts-check
-
-import axios from 'axios';
-import LinkHeader from 'http-link-header';
-
-import ready from './ready';
-/**
- * @param {import('axios').AxiosResponse} response
- * @returns {LinkHeader}
- */
-export const getLinks = response => {
-  const value = response.headers.link;
-
-  if (!value) {
-    return new LinkHeader();
-  }
-
-  return LinkHeader.parse(value);
-};
-
-/** @type {import('axios').RawAxiosRequestHeaders} */
-const csrfHeader = {};
-
-/**
- * @returns {void}
- */
-const setCSRFHeader = () => {
-  /** @type {HTMLMetaElement | null} */
-  const csrfToken = document.querySelector('meta[name=csrf-token]');
-
-  if (csrfToken) {
-    csrfHeader['X-CSRF-Token'] = csrfToken.content;
-  }
-};
-
-ready(setCSRFHeader);
-
-/**
- * @param {() => import('immutable').Map<string,any>} getState
- * @returns {import('axios').RawAxiosRequestHeaders}
- */
-const authorizationHeaderFromState = getState => {
-  const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
-
-  if (!accessToken) {
-    return {};
-  }
-
-  return {
-    'Authorization': `Bearer ${accessToken}`,
-  };
-};
-
-/**
- * @param {() => import('immutable').Map<string,any>} getState
- * @returns {import('axios').AxiosInstance}
- */
-export default function api(getState) {
-  return axios.create({
-    headers: {
-      ...csrfHeader,
-      ...authorizationHeaderFromState(getState),
-    },
-
-    transformResponse: [
-      function (data) {
-        try {
-          return JSON.parse(data);
-        } catch {
-          return data;
-        }
-      },
-    ],
-  });
-}
diff --git a/app/javascript/flavours/glitch/api.ts b/app/javascript/flavours/glitch/api.ts
new file mode 100644
index 0000000000..f262fd8570
--- /dev/null
+++ b/app/javascript/flavours/glitch/api.ts
@@ -0,0 +1,63 @@
+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';
+
+export const getLinks = (response: AxiosResponse) => {
+  const value = response.headers.link as string | undefined;
+
+  if (!value) {
+    return new LinkHeader();
+  }
+
+  return LinkHeader.parse(value);
+};
+
+const csrfHeader: RawAxiosRequestHeaders = {};
+
+const setCSRFHeader = () => {
+  const csrfToken = document.querySelector<HTMLMetaElement>(
+    'meta[name=csrf-token]',
+  );
+
+  if (csrfToken) {
+    csrfHeader['X-CSRF-Token'] = csrfToken.content;
+  }
+};
+
+void ready(setCSRFHeader);
+
+const authorizationHeaderFromState = (getState?: GetState) => {
+  const accessToken =
+    getState && (getState().meta.get('access_token', '') as string);
+
+  if (!accessToken) {
+    return {};
+  }
+
+  return {
+    Authorization: `Bearer ${accessToken}`,
+  } as RawAxiosRequestHeaders;
+};
+
+// eslint-disable-next-line import/no-default-export
+export default function api(getState: GetState) {
+  return axios.create({
+    headers: {
+      ...csrfHeader,
+      ...authorizationHeaderFromState(getState),
+    },
+
+    transformResponse: [
+      function (data: unknown) {
+        try {
+          return JSON.parse(data as string) as unknown;
+        } catch {
+          return data;
+        }
+      },
+    ],
+  });
+}
diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
index 2fd7d56735..d98a3996d0 100644
--- a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
+++ b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js
@@ -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}));
   },
 
 });
diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js
index d1ccf9ac95..191448c0e8 100644
--- a/app/javascript/flavours/glitch/reducers/relationships.js
+++ b/app/javascript/flavours/glitch/reducers/relationships.js
@@ -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:
diff --git a/app/javascript/flavours/glitch/store/index.ts b/app/javascript/flavours/glitch/store/index.ts
index f748662794..c2629b0ed7 100644
--- a/app/javascript/flavours/glitch/store/index.ts
+++ b/app/javascript/flavours/glitch/store/index.ts
@@ -1,45 +1,8 @@
-import type { TypedUseSelectorHook } from 'react-redux';
-import { useDispatch, useSelector } from 'react-redux';
+export { store } from './store';
+export type { GetState, AppDispatch, RootState } from './store';
 
-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 {
+  createAppAsyncThunk,
+  useAppDispatch,
+  useAppSelector,
+} from './typed_functions';
diff --git a/app/javascript/flavours/glitch/store/store.ts b/app/javascript/flavours/glitch/store/store.ts
new file mode 100644
index 0000000000..6350885680
--- /dev/null
+++ b/app/javascript/flavours/glitch/store/store.ts
@@ -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;
diff --git a/app/javascript/flavours/glitch/store/typed_functions.ts b/app/javascript/flavours/glitch/store/typed_functions.ts
new file mode 100644
index 0000000000..d05a256bab
--- /dev/null
+++ b/app/javascript/flavours/glitch/store/typed_functions.ts
@@ -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 };
+}>();