Convert js files in `client/src` to Typescript

Gitlab: #30
Change-Id: I679b8b0f30445a872d152ae93ecad5ff6c9a259f
diff --git a/.eslintrc.json b/.eslintrc.json
index 88a003f..323d257 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -33,6 +33,7 @@
     "@typescript-eslint/no-explicit-any": "off",
     "@typescript-eslint/no-unused-vars": "off",
     "eqeqeq": ["warn", "smart"],
+    "no-constant-condition": ["error", { "checkLoops": false }],
     "simple-import-sort/exports": "warn",
     "simple-import-sort/imports": "warn",
     "unused-imports/no-unused-imports": "error",
diff --git a/client/i18next-parser.config.js b/client/i18next-parser.config.js
index 2f9d13c..1e18e78 100644
--- a/client/i18next-parser.config.js
+++ b/client/i18next-parser.config.js
@@ -1,5 +1,5 @@
 export default {
   locales: ['fr', 'en'],
   output: 'public/locale/$LOCALE/$NAMESPACE.json',
-  input: ['src/**/*.{js,jsx}'],
+  input: ['src/**/*.{ts,tsx,js,jsx}'],
 };
diff --git a/client/src/App.test.js b/client/src/App.test.tsx
similarity index 100%
rename from client/src/App.test.js
rename to client/src/App.test.tsx
diff --git a/client/src/App.js b/client/src/App.tsx
similarity index 91%
rename from client/src/App.js
rename to client/src/App.tsx
index 7cec1ee..0893dbd 100644
--- a/client/src/App.js
+++ b/client/src/App.tsx
@@ -23,9 +23,7 @@
 // import { useSelector, useDispatch } from 'react-redux'
 // import { useAppSelector, useAppDispatch } from '../redux/hooks'
 
-const Home = (props) => {
-  console.log(`home ${props}`);
-
+const Home = () => {
   return <Navigate to="/account" />;
 };
 
@@ -57,7 +55,7 @@
       <Routes>
         <Route path="/setup" element={<ServerSetup />} />
         <Route path="/" element={<Navigate to="/setup" replace />} />
-        <Route index path="*" element={<Navigate to="/setup" replace />} />
+        <Route path="*" element={<Navigate to="/setup" replace />} />
       </Routes>
     );
   }
@@ -68,7 +66,7 @@
         <Route path="/account">
           <Route index element={<AccountSelection />} />
           <Route path=":accountId">
-            <Route index path="*" element={<JamiMessenger />} />
+            <Route path="*" element={<JamiMessenger />} />
             <Route path="settings" element={<AccountSettings />} />
           </Route>
         </Route>
@@ -78,7 +76,7 @@
         {/* <Route path="/Contacts" element={<ContactList />} /> */}
         <Route path="/Theme" element={<ThemeDemonstrator />} />
         <Route path="/setup" element={<ServerSetup />} />
-        <Route path="/" index element={<Home />} />
+        <Route path="/" element={<Home />} />
         <Route path="*" element={<NotFoundPage />} />
       </Routes>
       {!state.auth.authenticated && <SignInPage key="signin" open={!state.auth.authenticated} />}
diff --git a/client/src/AuthManager.js b/client/src/AuthManager.ts
similarity index 64%
rename from client/src/AuthManager.js
rename to client/src/AuthManager.ts
index ea55c3a..77d1945 100644
--- a/client/src/AuthManager.js
+++ b/client/src/AuthManager.ts
@@ -1,6 +1,8 @@
 /* eslint-disable no-undef */
 // TODO: This hides eslint errors for this file. This should be removed once this file is cleaned up.
 
+import { PromiseExecutor } from '../../model/util';
+
 /*
  *  Copyright (c) 2017-2021 Savoir-faire Linux Inc.
  *
@@ -20,12 +22,38 @@
  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
+interface AuthManagerState {
+  initialized: boolean;
+  authenticated: boolean;
+  setupComplete: boolean;
+  error: boolean;
+}
+
+interface AuthManagerTask extends PromiseExecutor<Response> {
+  url: string;
+  init?: RequestInit;
+}
+
+interface InitData {
+  loggedin?: true;
+  username?: string;
+  type?: string;
+  setupComplete?: boolean;
+}
+
+type OnAuthChanged = (auth: AuthManagerState) => void;
+
 class AuthManager {
+  private authenticating: boolean;
+  private readonly _state: AuthManagerState;
+  private tasks: AuthManagerTask[];
+  private onAuthChanged: OnAuthChanged | undefined;
+
   constructor() {
     console.log('AuthManager()');
     this.authenticating = false;
 
-    this.state = {
+    this._state = {
       initialized: false,
       authenticated: true,
       setupComplete: true,
@@ -35,46 +63,48 @@
     this.tasks = [];
     this.onAuthChanged = undefined;
 
+    // @ts-ignore
     if (initData) {
       console.log('Using static initData');
+      // @ts-ignore
       this.setInitData(initData);
       return;
     }
   }
 
   isAuthenticated() {
-    return this.state.authenticated;
+    return this._state.authenticated;
   }
 
   getState() {
-    return this.state;
+    return this._state;
   }
 
-  setInitData(data) {
+  setInitData(data: InitData) {
     this.authenticating = false;
-    this.state.initialized = true;
+    this._state.initialized = true;
     if (data.username) {
-      Object.assign(this.state, {
+      Object.assign(this._state, {
         authenticated: true,
         setupComplete: true,
         error: false,
         user: { username: data.username, type: data.type },
       });
     } else {
-      Object.assign(this.state, {
+      Object.assign(this._state, {
         authenticated: false,
-        setupComplete: 'setupComplete' in data ? data.setupComplete : true,
+        setupComplete: data.setupComplete ?? true,
         error: false,
       });
     }
     console.log('Init ended');
     /*if (this.onAuthChanged)
-            this.onAuthChanged(this.state)*/
+            this.onAuthChanged(this._state)*/
   }
 
-  init(cb) {
+  init(cb: OnAuthChanged) {
     this.onAuthChanged = cb;
-    if (this.state.initialized || this.authenticating) return;
+    if (this._state.initialized || this.authenticating) return;
     /*if (initData) {
             console.log("Using static initData")
             this.setInitData(initData)
@@ -84,14 +114,14 @@
     fetch('/auth')
       .then(async (response) => {
         this.authenticating = false;
-        this.state.initialized = true;
+        this._state.initialized = true;
         if (response.status === 200) {
           this.setInitData(await response.json());
         } else if (response.status === 401) {
           this.setInitData(await response.json());
         } else {
-          this.state.error = true;
-          if (this.onAuthChanged) this.onAuthChanged(this.state);
+          this._state.error = true;
+          if (this.onAuthChanged) this.onAuthChanged(this._state);
         }
       })
       .catch((e) => {
@@ -105,8 +135,8 @@
     this.onAuthChanged = undefined;
   }
 
-  async setup(password) {
-    if (this.authenticating || this.state.setupComplete) return;
+  async setup(password: string) {
+    if (this.authenticating || this._state.setupComplete) return;
     console.log('Starting setup');
     this.authenticating = true;
     const response = await fetch(`/setup`, {
@@ -124,12 +154,12 @@
     }
 
     this.authenticating = false;
-    this.state.setupComplete = true;
-    if (this.onAuthChanged) this.onAuthChanged(this.state);
+    this._state.setupComplete = true;
+    if (this.onAuthChanged) this.onAuthChanged(this._state);
     return response.ok;
   }
 
-  authenticate(username, password) {
+  authenticate(username: string, password: string) {
     if (this.authenticating) return;
     console.log('Starting authentication');
     this.authenticating = true;
@@ -139,15 +169,20 @@
       .then((response) => {
         console.log(response);
         this.authenticating = false;
-        this.state.authenticated = response.ok && response.status === 200;
-        if (this.onAuthChanged) this.onAuthChanged(this.state);
-        while (this.tasks.length !== 0) {
+        this._state.authenticated = response.ok && response.status === 200;
+        if (this.onAuthChanged) this.onAuthChanged(this._state);
+        while (true) {
           const task = this.tasks.shift();
-          if (this.state.authenticated)
+          if (!task) {
+            break;
+          }
+          if (this._state.authenticated) {
             fetch(task.url, task.init)
               .then((res) => task.resolve(res))
               .catch((e) => console.log('Error executing pending task: ' + e));
-          else task.reject(new Error('Authentication failed'));
+          } else {
+            task.reject(new Error('Authentication failed'));
+          }
         }
       })
       .catch((e) => {
@@ -158,17 +193,17 @@
 
   disconnect() {
     console.log('Disconnect');
-    this.state.authenticated = false;
-    if (this.onAuthChanged) this.onAuthChanged(this.state);
+    this._state.authenticated = false;
+    if (this.onAuthChanged) this.onAuthChanged(this._state);
   }
 
-  fetch(url, init) {
+  fetch(url: string, init?: RequestInit): Promise<Response> {
     console.log(`fetch ${url}`);
-    if (!this.state.authenticated) {
+    if (!this._state.authenticated) {
       if (!init || !init.method || init.method === 'GET') {
-        return new Promise((resolve, reject) => this.tasks.push({ url, init, resolve, reject }));
+        return new Promise<Response>((resolve, reject) => this.tasks.push({ url, init, resolve, reject }));
       } else {
-        return new Promise((resolve, reject) => reject('Not authenticated'));
+        return new Promise<Response>((resolve, reject) => reject('Not authenticated'));
       }
     }
     return fetch(url, init).then((response) => {
diff --git a/client/src/i18n.js b/client/src/i18n.ts
similarity index 100%
rename from client/src/i18n.js
rename to client/src/i18n.ts
diff --git a/client/src/index.js b/client/src/index.tsx
similarity index 85%
rename from client/src/index.js
rename to client/src/index.tsx
index 2d4d46b..9a68d34 100644
--- a/client/src/index.js
+++ b/client/src/index.tsx
@@ -1,3 +1,4 @@
+/// <reference types="webpack/module" />
 'use strict';
 import './index.scss';
 import './i18n';
@@ -5,13 +6,14 @@
 // import config from "../sentry-client.config.json"
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { StrictMode } from 'react';
+import { render } from 'react-dom';
 import { createRoot } from 'react-dom/client';
 import { Provider } from 'react-redux';
 import { BrowserRouter as Router } from 'react-router-dom';
 import socketio from 'socket.io-client';
 
 import { store } from '../redux/store';
-import App from './App.js';
+import App from './App';
 import { SocketProvider } from './contexts/socket.js';
 
 const queryClient = new QueryClient({
@@ -25,6 +27,9 @@
 const socket = socketio();
 
 const container = document.getElementById('root');
+if (!container) {
+  throw new Error('Failed to get the root element');
+}
 const root = createRoot(container);
 root.render(
   <Provider store={store}>
@@ -43,9 +48,7 @@
 if (import.meta.webpackHot)
   import.meta.webpackHot.accept('./App', () => {
     try {
-      // TODO: This needs be fixed
-      // eslint-disable-next-line no-undef
-      render(App);
+      render(<App />, container);
     } catch (e) {
       location.reload();
     }
diff --git a/client/webpack.config.js b/client/webpack.config.js
index 583b3aa..23781a2 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -13,7 +13,7 @@
 
 const mode = process.env.NODE_ENV || 'development';
 
-let entry = [resolve(__dirname, 'src', 'index.js')];
+let entry = [resolve(__dirname, 'src', 'index.tsx')];
 let plugins = [
   new HtmlWebpackPlugin({
     template: '!!raw-loader!' + resolve(__dirname, 'src', 'index.ejs'),