Convert all pages to Typescript

Convert all files in `client/src/pages` to Typescript

Gitlab: #30

Change-Id: I9b5ec5b042487d732bb7d46b584f797049eb068c
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 595fd70..b0fd67a 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -9,14 +9,14 @@
 
 import authManager from './AuthManager';
 import WelcomeAnimation from './components/welcome';
-import NotFoundPage from './pages/404.jsx';
-import AccountCreationDialog from './pages/accountCreation.jsx';
-import AccountSelection from './pages/accountSelection.jsx';
-import AccountSettings from './pages/accountSettings.jsx';
-import JamiAccountDialog from './pages/jamiAccountCreation.jsx';
-import JamiMessenger from './pages/JamiMessenger.jsx';
-import SignInPage from './pages/loginDialog.jsx';
-import ServerSetup from './pages/serverSetup.jsx';
+import NotFoundPage from './pages/404';
+import AccountCreationDialog from './pages/AccountCreation';
+import AccountSelection from './pages/AccountSelection';
+import AccountSettings from './pages/AccountSettings';
+import JamiAccountDialog from './pages/JamiAccountCreation';
+import JamiMessenger from './pages/JamiMessenger';
+import SignInPage from './pages/LoginDialog';
+import ServerSetup from './pages/ServerSetup';
 import defaultTheme from './themes/default';
 import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
 
diff --git a/client/src/components/AccountPreferences.js b/client/src/components/AccountPreferences.tsx
similarity index 89%
rename from client/src/components/AccountPreferences.js
rename to client/src/components/AccountPreferences.tsx
index 23b23f6..598d07f 100644
--- a/client/src/components/AccountPreferences.js
+++ b/client/src/components/AccountPreferences.tsx
@@ -21,6 +21,7 @@
 import { useState } from 'react';
 
 import Account from '../../../model/Account';
+import AccountDetails from '../../../model/AccountDetails';
 import authManager from '../AuthManager';
 import ConversationAvatar from './ConversationAvatar';
 import ConversationsOverviewCard from './ConversationsOverviewCard';
@@ -34,14 +35,19 @@
   exit: {
     scale: 0.5,
     opacity: 0,
-    transition: { duration: 1.5, ...transition },
+    transition: { ...transition, duration: 1.5 },
   },
 };
 
-export default function AccountPreferences(props) {
-  const account = props.account;
-  let devices = [];
-  for (var i in account.devices) devices.push([i, account.devices[i]]);
+type AccountPreferencesProps = {
+  account: Account;
+  onAccountChanged?: (account: Account) => void;
+};
+
+export default function AccountPreferences({ account }: AccountPreferencesProps) {
+  const devices: string[][] = [];
+  const accountDevices = account.getDevices();
+  for (const i in accountDevices) devices.push([i, accountDevices[i]]);
 
   console.log(devices);
 
@@ -59,12 +65,12 @@
     }
   };
 
-  const removeModerator = (uri) =>
+  const removeModerator = (uri: string) =>
     authManager.fetch(`/api/accounts/${account.getId()}/defaultModerators/${uri}`, { method: 'DELETE' });
 
-  const handleToggle = (key, value) => {
+  const handleToggle = (key: keyof AccountDetails, value: boolean) => {
     console.log(`handleToggle ${key} ${value}`);
-    const newDetails = {};
+    const newDetails: Partial<AccountDetails> = {};
     newDetails[key] = value ? 'true' : 'false';
     console.log(newDetails);
     authManager.fetch(`/api/accounts/${account.getId()}`, {
@@ -213,13 +219,13 @@
                 </ListItem>
               ) : (
                 moderators.map((moderator) => (
-                  <ListItem key={moderator.uri}>
+                  <ListItem key={moderator.getUri()}>
                     <ListItemAvatar>
-                      <ConversationAvatar name={moderator.getDisplayName()} />
+                      <ConversationAvatar displayName={moderator.getDisplayName()} />
                     </ListItemAvatar>
                     <ListItemText primary={moderator.getDisplayName()} />
                     <ListItemSecondaryAction>
-                      <IconButton onClick={(e) => removeModerator(moderator.uri)} size="large">
+                      <IconButton onClick={(e) => removeModerator(moderator.getUri())} size="large">
                         <DeleteRounded />
                       </IconButton>
                     </ListItemSecondaryAction>
diff --git a/client/src/pages/404.jsx b/client/src/pages/404.tsx
similarity index 100%
rename from client/src/pages/404.jsx
rename to client/src/pages/404.tsx
diff --git a/client/src/pages/accountCreation.jsx b/client/src/pages/AccountCreation.tsx
similarity index 96%
rename from client/src/pages/accountCreation.jsx
rename to client/src/pages/AccountCreation.tsx
index b8c323b..30a22ee 100644
--- a/client/src/pages/accountCreation.jsx
+++ b/client/src/pages/AccountCreation.tsx
@@ -3,7 +3,7 @@
 
 import ListItemLink from '../components/ListItemLink';
 
-export default function AccountCreationDialog(props) {
+export default function AccountCreationDialog() {
   return (
     <Container>
       <Card>
diff --git a/client/src/pages/accountSelection.jsx b/client/src/pages/AccountSelection.tsx
similarity index 95%
rename from client/src/pages/accountSelection.jsx
rename to client/src/pages/AccountSelection.tsx
index e8bbf1e..9f6fcc8 100644
--- a/client/src/pages/accountSelection.jsx
+++ b/client/src/pages/AccountSelection.tsx
@@ -16,11 +16,11 @@
   exit: { opacity: 0, y: '-50px' },
 };
 
-const AccountSelection = (props) => {
+const AccountSelection = () => {
   const navigate = useNavigate();
   const [loaded, setLoaded] = useState(false);
   const [error, setError] = useState(false);
-  const [accounts, setAccounts] = useState([]);
+  const [accounts, setAccounts] = useState<Account[]>([]);
 
   useEffect(() => {
     const controller = new AbortController();
@@ -28,7 +28,7 @@
       .fetch(`/api/accounts`, { signal: controller.signal })
       .then((res) => res.json())
       .then(
-        (result) => {
+        (result: Account[]) => {
           console.log(result);
           if (result.length === 0) {
             navigate('/newAccount');
diff --git a/client/src/pages/accountSettings.jsx b/client/src/pages/AccountSettings.tsx
similarity index 67%
rename from client/src/pages/accountSettings.jsx
rename to client/src/pages/AccountSettings.tsx
index 10dab29..0e715b5 100644
--- a/client/src/pages/accountSettings.jsx
+++ b/client/src/pages/AccountSettings.tsx
@@ -9,15 +9,23 @@
 import AccountPreferences from '../components/AccountPreferences';
 import Header from '../components/Header';
 
-const AccountSettings = (props) => {
+type AccountSettingsProps = {
+  accountId?: string;
+  account?: Account;
+};
+
+const AccountSettings = (props: AccountSettingsProps) => {
   console.log('ACCOUNT SETTINGS', props.account);
-  let accountId = useParams().accountId;
-  if (props.accountId) {
-    accountId = props.accountId;
+  const params = useParams();
+  const accountId = props.accountId || params.accountId;
+
+  if (accountId == null) {
+    throw new Error('Missing accountId');
   }
+
   const dispatch = useAppDispatch();
 
-  const [state, setState] = useState({ loaded: false });
+  const [account, setAccount] = useState<Account | null>(null);
 
   useEffect(() => {
     dispatch(setAccountId(accountId));
@@ -27,10 +35,10 @@
       .fetch(`/api/accounts/${accountId}`, { signal: controller.signal })
       .then((res) => res.json())
       .then((result) => {
-        let account = Account.from(result);
+        const account = Account.from(result);
         account.setDevices(result.devices);
         dispatch(setAccountObject(account));
-        setState({ loaded: true, account: account });
+        setAccount(account);
       })
       .catch((e) => console.log(e));
     // return () => controller.abort() // crash on React18
@@ -39,16 +47,8 @@
   return (
     <Container maxWidth="sm">
       <Header />
-      {state.loaded ? (
-        <AccountPreferences
-          account={state.account}
-          onAccontChanged={(account) =>
-            setState((state) => {
-              state.account = account;
-              return state;
-            })
-          }
-        />
+      {account != null ? (
+        <AccountPreferences account={account} onAccountChanged={(account: Account) => setAccount(account)} />
       ) : (
         <CircularProgress />
       )}
diff --git a/client/src/pages/addContactPage.jsx b/client/src/pages/AddContactPage.tsx
similarity index 79%
rename from client/src/pages/addContactPage.jsx
rename to client/src/pages/AddContactPage.tsx
index c1b242f..2dfae73 100644
--- a/client/src/pages/addContactPage.jsx
+++ b/client/src/pages/AddContactPage.tsx
@@ -1,18 +1,25 @@
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Typography } from '@mui/material';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
 
 import { setRefreshFromSlice } from '../../redux/appSlice';
 import { useAppDispatch } from '../../redux/hooks';
 import authManager from '../AuthManager';
 
-export default function AddContactPage(props) {
+type AddContactPageProps = {
+  accountId: string;
+  contactId: string;
+};
+
+export default function AddContactPage(props: AddContactPageProps) {
   const navigate = useNavigate();
-  const accountId = props.accountId || props.match.params.accountId;
-  const contactId = props.contactId || props.match.params.contactId;
+
+  const params = useParams();
+  const accountId = props.accountId || params.accountId;
+  const contactId = props.contactId || params.contactId;
   const dispatch = useAppDispatch();
 
-  const handleClick = async (e) => {
+  const handleClick = async () => {
     const response = await authManager
       .fetch(`/api/accounts/${accountId}/conversations`, {
         method: 'POST',
diff --git a/client/src/pages/jamiAccountCreation.jsx b/client/src/pages/JamiAccountCreation.tsx
similarity index 93%
rename from client/src/pages/jamiAccountCreation.jsx
rename to client/src/pages/JamiAccountCreation.tsx
index 09f8612..8360e06 100644
--- a/client/src/pages/jamiAccountCreation.jsx
+++ b/client/src/pages/JamiAccountCreation.tsx
@@ -1,18 +1,18 @@
 import { AddRounded } from '@mui/icons-material';
 import { Box, Card, CardActions, CardContent, Container, Fab, Typography } from '@mui/material';
-import { useState } from 'react';
+import { FormEvent, useState } from 'react';
 import { useNavigate } from 'react-router';
 
 import authManager from '../AuthManager';
 import UsernameChooser from '../components/UsernameChooser';
 
-export default function JamiAccountDialog(props) {
+export default function JamiAccountDialog() {
   const [name, setName] = useState('');
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState(false);
   const navigate = useNavigate();
 
-  const onSubmit = async (event) => {
+  const onSubmit = async (event: FormEvent) => {
     event.preventDefault();
     setLoading(true);
     const result = await authManager
diff --git a/client/src/pages/JamiMessenger.jsx b/client/src/pages/JamiMessenger.tsx
similarity index 62%
rename from client/src/pages/JamiMessenger.jsx
rename to client/src/pages/JamiMessenger.tsx
index 017a9b9..e2709b9 100644
--- a/client/src/pages/JamiMessenger.jsx
+++ b/client/src/pages/JamiMessenger.tsx
@@ -1,13 +1,13 @@
 import { Route, Routes } from 'react-router-dom';
 
-import Messenger from './messenger.jsx';
+import Messenger from './Messenger';
 
-export default function JamiMessenger(props) {
+export default function JamiMessenger() {
   return (
     <Routes>
       <Route path="addContact/:contactId" element={<Messenger />} />
       <Route path="conversation/:conversationId" element={<Messenger />} />
-      <Route index path="*" element={<Messenger />} />
+      <Route path="*" element={<Messenger />} />
     </Routes>
   );
 }
diff --git a/client/src/pages/loginDialog.jsx b/client/src/pages/LoginDialog.tsx
similarity index 85%
rename from client/src/pages/loginDialog.jsx
rename to client/src/pages/LoginDialog.tsx
index 34070d7..e98cc79 100644
--- a/client/src/pages/loginDialog.jsx
+++ b/client/src/pages/LoginDialog.tsx
@@ -8,7 +8,7 @@
 import Link from '@mui/material/Link';
 import TextField from '@mui/material/TextField';
 import Typography from '@mui/material/Typography';
-import { Component } from 'react';
+import { ChangeEvent, Component, MouseEvent } from 'react';
 
 import authManager from '../AuthManager';
 
@@ -26,8 +26,22 @@
   );
 }
 
-class SignInPage extends Component {
-  constructor(props) {
+type SignInPageProps = {
+  open: boolean;
+};
+type SignInPageState = {
+  username?: string;
+  password?: string;
+  submitted?: boolean;
+  loading?: boolean;
+  redirect?: boolean;
+  error?: boolean;
+  open?: boolean;
+  errorMessage?: string;
+};
+
+class SignInPage extends Component<SignInPageProps, SignInPageState> {
+  constructor(props: SignInPageProps) {
     console.log('SignInPage ' + props.open);
     super(props);
     this.state = {
@@ -38,11 +52,11 @@
     this.localLogin = this.localLogin.bind(this);
   }
 
-  handleusername(text) {
+  handleusername(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
     this.setState({ username: text.target.value });
   }
 
-  handlePassword(text) {
+  handlePassword(text: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
     this.setState({ password: text.target.value });
   }
 
@@ -82,11 +96,12 @@
             })*/
   }
 
-  handleSubmit(event) {
+  handleSubmit(event: MouseEvent<HTMLButtonElement>) {
     event.preventDefault();
-    let obj = {};
-    obj.username = this.state.username;
-    obj.password = this.state.password;
+    const obj = {
+      username: this.state.username,
+      password: this.state.password,
+    };
 
     this.setState({
       submitted: true,
@@ -94,7 +109,7 @@
     });
 
     fetch('/api/login?username=' + obj.username + '&password=' + obj.password, {
-      header: {
+      headers: {
         'Content-Type': 'application/json',
       },
       method: 'POST',
@@ -102,11 +117,11 @@
       //body: JSON.stringify({ obj })
     })
       .then((res) => {
-        if (res.status === '200') {
+        if (res.status === 200) {
           this.setState({
             redirect: true,
           });
-        } else if (res.status === '401') {
+        } else if (res.status === 401) {
           this.setState({
             loading: false,
             error: true,
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/Messenger.tsx
similarity index 84%
rename from client/src/pages/messenger.jsx
rename to client/src/pages/Messenger.tsx
index 3108fe6..c9449cd 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/Messenger.tsx
@@ -12,27 +12,37 @@
 import Header from '../components/Header';
 import LoadingPage from '../components/loading';
 import NewContactForm from '../components/NewContactForm';
-import AddContactPage from './addContactPage.jsx';
+import AddContactPage from './AddContactPage';
 
-const Messenger = (props) => {
+type MessengerProps = {
+  accountId?: string;
+  conversationId?: string;
+  contactId?: string;
+};
+
+const Messenger = (props: MessengerProps) => {
   const { refresh } = useAppSelector((state) => state.app);
 
-  const [conversations, setConversations] = useState(undefined);
+  const [conversations, setConversations] = useState<Conversation[] | undefined>(undefined);
   const [searchQuery, setSearchQuery] = useState('');
-  const [searchResult, setSearchResults] = useState(undefined);
+  const [searchResult, setSearchResults] = useState<Conversation | undefined>(undefined);
 
   const params = useParams();
   const accountId = props.accountId || params.accountId;
   const conversationId = props.conversationId || params.conversationId;
   const contactId = props.contactId || params.contactId;
 
+  if (accountId == null) {
+    throw new Error('Missing accountId');
+  }
+
   useEffect(() => {
     console.log('REFRESH CONVERSATIONS FROM MESSENGER');
     const controller = new AbortController();
     authManager
       .fetch(`/api/accounts/${accountId}/conversations`, { signal: controller.signal })
       .then((res) => res.json())
-      .then((result) => {
+      .then((result: Conversation[]) => {
         console.log(result);
         setConversations(Object.values(result).map((c) => Conversation.from(accountId, c)));
       });
@@ -48,7 +58,7 @@
         if (response.status === 200) {
           return response.json();
         } else {
-          throw new Error(response.status);
+          throw new Error(response.status.toString());
         }
       })
       .then((response) => {
diff --git a/client/src/pages/serverConfiguration.jsx b/client/src/pages/ServerConfiguration.tsx
similarity index 68%
rename from client/src/pages/serverConfiguration.jsx
rename to client/src/pages/ServerConfiguration.tsx
index 829977e..e9e5328 100644
--- a/client/src/pages/serverConfiguration.jsx
+++ b/client/src/pages/ServerConfiguration.tsx
@@ -1,16 +1,21 @@
 import CircularProgress from '@mui/material/CircularProgress';
 import Container from '@mui/material/Container';
 import { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
 
 import Account from '../../../model/Account';
 import authManager from '../AuthManager';
 import AccountPreferences from '../components/AccountPreferences';
 import Header from '../components/Header';
 
-const ServerOverview = (props) => {
-  const [loaded, setLoaded] = useState(false);
-  const [account, setAccount] = useState();
-  const accountId = props.accountId || props.match.params.accountId;
+type ServerOverviewProps = {
+  accountId?: string;
+};
+
+const ServerOverview = (props: ServerOverviewProps) => {
+  const [account, setAccount] = useState<Account | null>(null);
+  const params = useParams();
+  const accountId = props.accountId || params.accountId;
 
   useEffect(() => {
     const controller = new AbortController();
@@ -19,7 +24,6 @@
       .then((res) => res.json())
       .then((result) => {
         console.log(result);
-        setLoaded(true);
         setAccount(Account.from(result));
       })
       .catch((e) => console.log(e));
@@ -29,7 +33,7 @@
   return (
     <Container maxWidth="sm" className="app">
       <Header />
-      {loaded ? <AccountPreferences account={account} /> : <CircularProgress />}
+      {account != null ? <AccountPreferences account={account} /> : <CircularProgress />}
     </Container>
   );
 };
diff --git a/client/src/pages/serverSetup.jsx b/client/src/pages/ServerSetup.tsx
similarity index 89%
rename from client/src/pages/serverSetup.jsx
rename to client/src/pages/ServerSetup.tsx
index b7f79a7..dd414d3 100644
--- a/client/src/pages/serverSetup.jsx
+++ b/client/src/pages/ServerSetup.tsx
@@ -1,17 +1,17 @@
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
-import { useState } from 'react';
+import { FormEvent, useState } from 'react';
 
 import authManager from '../AuthManager';
 
-export default function ServerSetup(props) {
+export default function ServerSetup() {
   const [password, setPassword] = useState('');
   const [passwordRepeat, setPasswordRepeat] = useState('');
   const [loading, setLoading] = useState(false);
 
   const isValid = () => password && password === passwordRepeat;
 
-  const handleSubmit = (e) => {
+  const handleSubmit = (e: FormEvent) => {
     e.preventDefault();
     setLoading(true);
     if (!isValid()) return;
@@ -20,7 +20,7 @@
 
   return (
     <Container className="message-list">
-      <Card disabled={loading}>
+      <Card>
         <CardContent component="form" onSubmit={handleSubmit}>
           <Typography gutterBottom variant="h5" component="h2">
             Jami Web Node setup
@@ -43,6 +43,7 @@
                 type="password"
                 placeholder="New password"
                 autoComplete="new-password"
+                disabled={loading}
               />
             </div>
             <div>
@@ -54,11 +55,12 @@
                 type="password"
                 placeholder="Repeat password"
                 autoComplete="new-password"
+                disabled={loading}
               />
             </div>
           </Box>
           <Box style={{ textAlign: 'center', marginTop: 24 }}>
-            <Fab variant="extended" color="primary" type="submit" disabled={!isValid()}>
+            <Fab variant="extended" color="primary" type="submit" disabled={!isValid() || loading}>
               <GroupAddRounded />
               Create admin account
             </Fab>