From 1142f4c79e3eaf4450ed727de0f480e300e8b9a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= <git@joshuakgoldberg.com>
Date: Tue, 28 Nov 2023 18:47:55 +0100
Subject: [PATCH] Converted app/javascript/mastodon/utils/ folder to TypeScript
 (#27895)

---
 .../{base64-test.js => base64-test.ts}        |  0
 .../__tests__/{html-test.js => html-test.s}   |  0
 app/javascript/mastodon/utils/config.js       | 10 -----
 app/javascript/mastodon/utils/config.ts       | 13 ++++++
 app/javascript/mastodon/utils/html.js         |  6 ---
 app/javascript/mastodon/utils/html.ts         |  9 +++++
 .../mastodon/utils/{icons.jsx => icons.tsx}   | 14 ++++++-
 .../mastodon/utils/{log_out.js => log_out.ts} |  0
 .../mastodon/utils/notifications.js           | 30 --------------
 .../mastodon/utils/notifications.ts           | 13 ++++++
 .../{react_router.jsx => react_router.tsx}    | 40 +++++++++++--------
 .../utils/{scrollbar.js => scrollbar.ts}      | 15 +++----
 package.json                                  |  1 +
 yarn.lock                                     |  8 ++++
 14 files changed, 84 insertions(+), 75 deletions(-)
 rename app/javascript/mastodon/utils/__tests__/{base64-test.js => base64-test.ts} (100%)
 rename app/javascript/mastodon/utils/__tests__/{html-test.js => html-test.s} (100%)
 delete mode 100644 app/javascript/mastodon/utils/config.js
 create mode 100644 app/javascript/mastodon/utils/config.ts
 delete mode 100644 app/javascript/mastodon/utils/html.js
 create mode 100644 app/javascript/mastodon/utils/html.ts
 rename app/javascript/mastodon/utils/{icons.jsx => icons.tsx} (69%)
 rename app/javascript/mastodon/utils/{log_out.js => log_out.ts} (100%)
 delete mode 100644 app/javascript/mastodon/utils/notifications.js
 create mode 100644 app/javascript/mastodon/utils/notifications.ts
 rename app/javascript/mastodon/utils/{react_router.jsx => react_router.tsx} (53%)
 rename app/javascript/mastodon/utils/{scrollbar.js => scrollbar.ts} (70%)

diff --git a/app/javascript/mastodon/utils/__tests__/base64-test.js b/app/javascript/mastodon/utils/__tests__/base64-test.ts
similarity index 100%
rename from app/javascript/mastodon/utils/__tests__/base64-test.js
rename to app/javascript/mastodon/utils/__tests__/base64-test.ts
diff --git a/app/javascript/mastodon/utils/__tests__/html-test.js b/app/javascript/mastodon/utils/__tests__/html-test.s
similarity index 100%
rename from app/javascript/mastodon/utils/__tests__/html-test.js
rename to app/javascript/mastodon/utils/__tests__/html-test.s
diff --git a/app/javascript/mastodon/utils/config.js b/app/javascript/mastodon/utils/config.js
deleted file mode 100644
index 932cd0cbf..000000000
--- a/app/javascript/mastodon/utils/config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import ready from '../ready';
-
-export let assetHost = '';
-
-ready(() => {
-  const cdnHost = document.querySelector('meta[name=cdn-host]');
-  if (cdnHost) {
-    assetHost = cdnHost.content || '';
-  }
-});
diff --git a/app/javascript/mastodon/utils/config.ts b/app/javascript/mastodon/utils/config.ts
new file mode 100644
index 000000000..9222c89d1
--- /dev/null
+++ b/app/javascript/mastodon/utils/config.ts
@@ -0,0 +1,13 @@
+import ready from '../ready';
+
+export let assetHost = '';
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+ready(() => {
+  const cdnHost = document.querySelector<HTMLMetaElement>(
+    'meta[name=cdn-host]',
+  );
+  if (cdnHost) {
+    assetHost = cdnHost.content || '';
+  }
+});
diff --git a/app/javascript/mastodon/utils/html.js b/app/javascript/mastodon/utils/html.js
deleted file mode 100644
index 247e98c88..000000000
--- a/app/javascript/mastodon/utils/html.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// NB: This function can still return unsafe HTML
-export const unescapeHTML = (html) => {
-  const wrapper = document.createElement('div');
-  wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
-  return wrapper.textContent;
-};
diff --git a/app/javascript/mastodon/utils/html.ts b/app/javascript/mastodon/utils/html.ts
new file mode 100644
index 000000000..0145a0455
--- /dev/null
+++ b/app/javascript/mastodon/utils/html.ts
@@ -0,0 +1,9 @@
+// NB: This function can still return unsafe HTML
+export const unescapeHTML = (html: string) => {
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = html
+    .replace(/<br\s*\/?>/g, '\n')
+    .replace(/<\/p><p>/g, '\n\n')
+    .replace(/<[^>]*>/g, '');
+  return wrapper.textContent;
+};
diff --git a/app/javascript/mastodon/utils/icons.jsx b/app/javascript/mastodon/utils/icons.tsx
similarity index 69%
rename from app/javascript/mastodon/utils/icons.jsx
rename to app/javascript/mastodon/utils/icons.tsx
index be566032e..6e432e32f 100644
--- a/app/javascript/mastodon/utils/icons.jsx
+++ b/app/javascript/mastodon/utils/icons.tsx
@@ -1,13 +1,23 @@
 // Copied from emoji-mart for consistency with emoji picker and since
 // they don't export the icons in the package
 export const loupeIcon = (
-  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
+  <svg
+    xmlns='http://www.w3.org/2000/svg'
+    viewBox='0 0 20 20'
+    width='13'
+    height='13'
+  >
     <path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
   </svg>
 );
 
 export const deleteIcon = (
-  <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
+  <svg
+    xmlns='http://www.w3.org/2000/svg'
+    viewBox='0 0 20 20'
+    width='13'
+    height='13'
+  >
     <path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
   </svg>
 );
diff --git a/app/javascript/mastodon/utils/log_out.js b/app/javascript/mastodon/utils/log_out.ts
similarity index 100%
rename from app/javascript/mastodon/utils/log_out.js
rename to app/javascript/mastodon/utils/log_out.ts
diff --git a/app/javascript/mastodon/utils/notifications.js b/app/javascript/mastodon/utils/notifications.js
deleted file mode 100644
index 42623ac7c..000000000
--- a/app/javascript/mastodon/utils/notifications.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// Handles browser quirks, based on
-// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
-
-const checkNotificationPromise = () => {
-  try {
-    // eslint-disable-next-line promise/valid-params, promise/catch-or-return
-    Notification.requestPermission().then();
-  } catch(e) {
-    return false;
-  }
-
-  return true;
-};
-
-const handlePermission = (permission, callback) => {
-  // Whatever the user answers, we make sure Chrome stores the information
-  if(!('permission' in Notification)) {
-    Notification.permission = permission;
-  }
-
-  callback(Notification.permission);
-};
-
-export const requestNotificationPermission = (callback) => {
-  if (checkNotificationPromise()) {
-    Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
-  } else {
-    Notification.requestPermission((permission) => handlePermission(permission, callback));
-  }
-};
diff --git a/app/javascript/mastodon/utils/notifications.ts b/app/javascript/mastodon/utils/notifications.ts
new file mode 100644
index 000000000..08f677f8f
--- /dev/null
+++ b/app/javascript/mastodon/utils/notifications.ts
@@ -0,0 +1,13 @@
+/**
+ * Tries Notification.requestPermission, console warning instead of rejecting on error.
+ * @param callback Runs with the permission result on completion.
+ */
+export const requestNotificationPermission = async (
+  callback: NotificationPermissionCallback,
+) => {
+  try {
+    callback(await Notification.requestPermission());
+  } catch (error) {
+    console.warn(error);
+  }
+};
diff --git a/app/javascript/mastodon/utils/react_router.jsx b/app/javascript/mastodon/utils/react_router.tsx
similarity index 53%
rename from app/javascript/mastodon/utils/react_router.jsx
rename to app/javascript/mastodon/utils/react_router.tsx
index fa8f0db2b..0682fee55 100644
--- a/app/javascript/mastodon/utils/react_router.jsx
+++ b/app/javascript/mastodon/utils/react_router.tsx
@@ -1,8 +1,8 @@
-import PropTypes from "prop-types";
+import PropTypes from 'prop-types';
 
-import { __RouterContext } from "react-router";
+import { __RouterContext } from 'react-router';
 
-import hoistStatics from "hoist-non-react-statics";
+import hoistStatics from 'hoist-non-react-statics';
 
 export const WithRouterPropTypes = {
   match: PropTypes.object.isRequired,
@@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = {
   history: PropTypes.object,
 };
 
+export interface OptionalRouterProps {
+  ref: unknown;
+  wrappedComponentRef: unknown;
+}
+
 // This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
 // but does not fail if called outside of a React Router context
-export function withOptionalRouter(Component) {
-  const displayName = `withRouter(${Component.displayName || Component.name})`;
-  const C = props => {
+export function withOptionalRouter<
+  ComponentType extends React.ComponentType<OptionalRouterProps>,
+>(Component: ComponentType) {
+  const displayName = `withRouter(${Component.displayName ?? Component.name})`;
+  const C = (props: React.ComponentProps<ComponentType>) => {
     const { wrappedComponentRef, ...remainingProps } = props;
 
     return (
       <__RouterContext.Consumer>
-        {context => {
-          if(context)
+        {(context) => {
+          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+          if (context) {
             return (
+              // @ts-expect-error - Dynamic covariant generic components are tough to type.
               <Component
                 {...remainingProps}
                 {...context}
                 ref={wrappedComponentRef}
               />
             );
-          else
-            return (
-              <Component
-                {...remainingProps}
-                ref={wrappedComponentRef}
-              />
-            );
+          } else {
+            // @ts-expect-error - Dynamic covariant generic components are tough to type.
+            return <Component {...remainingProps} ref={wrappedComponentRef} />;
+          }
         }}
       </__RouterContext.Consumer>
     );
@@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
     wrappedComponentRef: PropTypes.oneOfType([
       PropTypes.string,
       PropTypes.func,
-      PropTypes.object
-    ])
+      PropTypes.object,
+    ]),
   };
 
   return hoistStatics(C, Component);
diff --git a/app/javascript/mastodon/utils/scrollbar.js b/app/javascript/mastodon/utils/scrollbar.ts
similarity index 70%
rename from app/javascript/mastodon/utils/scrollbar.js
rename to app/javascript/mastodon/utils/scrollbar.ts
index ca87dd76f..d505df124 100644
--- a/app/javascript/mastodon/utils/scrollbar.js
+++ b/app/javascript/mastodon/utils/scrollbar.ts
@@ -1,11 +1,7 @@
 import { isMobile } from '../is_mobile';
 
-/** @type {number | null} */
-let cachedScrollbarWidth = null;
+let cachedScrollbarWidth: number | null = null;
 
-/**
- * @returns {number}
- */
 const getActualScrollbarWidth = () => {
   const outer = document.createElement('div');
   outer.style.visibility = 'hidden';
@@ -16,20 +12,19 @@ const getActualScrollbarWidth = () => {
   outer.appendChild(inner);
 
   const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
-  outer.parentNode.removeChild(outer);
+  outer.remove();
 
   return scrollbarWidth;
 };
 
-/**
- * @returns {number}
- */
 export const getScrollbarWidth = () => {
   if (cachedScrollbarWidth !== null) {
     return cachedScrollbarWidth;
   }
 
-  const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth();
+  const scrollbarWidth = isMobile(window.innerWidth)
+    ? 0
+    : getActualScrollbarWidth();
   cachedScrollbarWidth = scrollbarWidth;
 
   return scrollbarWidth;
diff --git a/package.json b/package.json
index 2f324f77a..543af8a4b 100644
--- a/package.json
+++ b/package.json
@@ -161,6 +161,7 @@
     "@types/object-assign": "^4.0.30",
     "@types/prop-types": "^15.7.5",
     "@types/punycode": "^2.1.0",
+    "@types/rails__ujs": "^6.0.4",
     "@types/react": "^18.2.7",
     "@types/react-dom": "^18.2.4",
     "@types/react-helmet": "^6.1.6",
diff --git a/yarn.lock b/yarn.lock
index 0dd9a627c..11c2724d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2317,6 +2317,7 @@ __metadata:
     "@types/object-assign": "npm:^4.0.30"
     "@types/prop-types": "npm:^15.7.5"
     "@types/punycode": "npm:^2.1.0"
+    "@types/rails__ujs": "npm:^6.0.4"
     "@types/react": "npm:^18.2.7"
     "@types/react-dom": "npm:^18.2.4"
     "@types/react-helmet": "npm:^6.1.6"
@@ -3349,6 +3350,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/rails__ujs@npm:^6.0.4":
+  version: 6.0.4
+  resolution: "@types/rails__ujs@npm:6.0.4"
+  checksum: 7477cb03a0e1339b9cd5c8ac4a197a153e2ff48742b2f527c5a39dcdf80f01493011e368483290d3717662c63066fada3ab203a335804cbb3573cf575f37007e
+  languageName: node
+  linkType: hard
+
 "@types/range-parser@npm:*":
   version: 1.2.7
   resolution: "@types/range-parser@npm:1.2.7"