add socket.io back, cleanup

Change-Id: I74e043268c23fb45371f1e397ca2931ca177afc3
diff --git a/client/package.json b/client/package.json
index 191dd7f..5c4cc03 100644
--- a/client/package.json
+++ b/client/package.json
@@ -17,7 +17,7 @@
     "react-emoji-render": "^1.2.4",
     "react-router-dom": "^5.2.0",
     "react-sound": "^1.2.0",
-    "socket.io-client": "^2.3.0"
+    "socket.io-client": "^4.0.1"
   },
   "devDependencies": {
     "@babel/plugin-transform-runtime": "^7.13.15",
diff --git a/client/src/AuthManager.js b/client/src/AuthManager.js
index 4b7b6b4..46b4466 100644
--- a/client/src/AuthManager.js
+++ b/client/src/AuthManager.js
@@ -142,7 +142,11 @@
     fetch(url, init) {
         console.log(`fetch ${url}`)
         if (!this.state.authenticated) {
-            return new Promise((resolve, reject) => this.tasks.push({url, init, resolve, reject}))
+            if (!init || !init.method || init.method === 'GET') {
+                return new Promise((resolve, reject) => this.tasks.push({url, init, resolve, reject}))
+            } else {
+                return new Promise((resolve, reject) => reject("Not authenticated"))
+            }
         }
         return fetch(url, init)
             .then(response => {
diff --git a/client/src/components/AccountPreferences.js b/client/src/components/AccountPreferences.js
index ee09094..e14101f 100644
--- a/client/src/components/AccountPreferences.js
+++ b/client/src/components/AccountPreferences.js
@@ -1,13 +1,15 @@
-import React from 'react'
+import React, { useState } from 'react'
 import { makeStyles } from '@material-ui/core/styles'
 import { List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, ListSubheader, Switch, Typography, Grid, Paper, CardContent, Card, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Toolbar, IconButton, ListItemAvatar, Input, TextField } from '@material-ui/core'
-import { PhoneCallbackRounded, GroupRounded, DeleteRounded, AccountCircle, AddCircle } from '@material-ui/icons'
+import { PhoneCallbackRounded, GroupRounded, DeleteRounded, AddCircle } from '@material-ui/icons'
 
 import Account from '../../../model/Account'
 import JamiIdCard from './JamiIdCard'
 import ConversationAvatar from './ConversationAvatar'
 import ConversationsOverviewCard from './ConversationsOverviewCard'
 
+import authManager from '../AuthManager'
+
 const useStyles = makeStyles(theme => ({
   root: {
     minWidth: 275,
@@ -35,6 +37,18 @@
   const isJamiAccount = account.getType() === Account.TYPE_JAMI
   const alias = isJamiAccount ? "Jami account" : "SIP account"
   const moderators = account.getDefaultModerators()
+  const [defaultModeratorUri, setDefaultModeratorUri] = useState('')
+
+  const addModerator = () => {
+    if (defaultModeratorUri) {
+      authManager.fetch(`/api/accounts/${account.getId()}/defaultModerators/${defaultModeratorUri}`, {method: "PUT"})
+      setDefaultModeratorUri('')
+    }
+  }
+
+  const removeModerator = (uri) =>
+    authManager.fetch(`/api/accounts/${account.getId()}/defaultModerators/${uri}`, {method: "DELETE"})
+
   return (
     <React.Fragment>
       <Typography variant="h2" component="h2" gutterBottom>{alias}</Typography>
@@ -98,22 +112,28 @@
           </Toolbar>
           <List>
             <ListItem key="add">
-              <TextField variant="outlined" className={classes.textField} label="Add new default moderator" placeholder="Enter new moderator name or URI" fullWidth />
+              <TextField variant="outlined"
+                className={classes.textField}
+                value={defaultModeratorUri}
+                onChange={e => setDefaultModeratorUri(e.target.value)}
+                label="Add new default moderator"
+                placeholder="Enter new moderator name or URI"
+                fullWidth />
               <ListItemSecondaryAction>
-                <IconButton><AddCircle /></IconButton>
+                <IconButton onClick={addModerator}><AddCircle /></IconButton>
               </ListItemSecondaryAction>
             </ListItem>
-            {moderators.length === 0 ?
+            {!moderators || moderators.length === 0 ?
               <ListItem key="placeholder">
                 <ListItemText primary="No default moderator" /></ListItem> :
               moderators.map((moderator) => (
-                <ListItem key={moderator.name}>
+                <ListItem key={moderator.uri}>
                   <ListItemAvatar>
-                    <ConversationAvatar name={moderator.name} />
+                    <ConversationAvatar name={moderator.getDisplayName()} />
                   </ListItemAvatar>
-                  <ListItemText primary={moderator.name} />
+                  <ListItemText primary={moderator.getDisplayName()} />
                   <ListItemSecondaryAction>
-                    <IconButton><DeleteRounded /></IconButton>
+                    <IconButton onClick={e => removeModerator(moderator.uri)}><DeleteRounded /></IconButton>
                   </ListItemSecondaryAction>
                 </ListItem>
               ))}
diff --git a/client/src/components/ConversationView.js b/client/src/components/ConversationView.js
index 719d3d0..6217905 100644
--- a/client/src/components/ConversationView.js
+++ b/client/src/components/ConversationView.js
@@ -8,7 +8,7 @@
 
 const ConversationView = props => {
   const [state, setState] = useState({
-    loaded:false,
+    loaded: false,
     error: false,
     conversation: undefined
   })
@@ -44,15 +44,26 @@
     })
   }
 
+  const loadMore = () => {
+    authManager.fetch(`/api/accounts/${props.accountId}/conversations/${props.conversationId}/messages`)
+      .then(res => res.json())
+      .then(messages => {
+        console.log(messages)
+        state.conversation.addLoadedMessages(messages)
+        setState(state)
+      })
+  }
+
+  console.log("ConversationView render " + (state.conversation ? state.conversation.getMessages().length : "no conversation"))
   if (state.loaded === false) {
       return <LoadingPage />
   } else if (state.error === true) {
       return <div>Error loding {props.conversationId}</div>
   } else {
-  return <React.Fragment>
-      <MessageList conversation={state.conversation} messages={state.conversation.getMessages()} />
+  return <div className="messenger">
+      <MessageList conversation={state.conversation} loadMore={loadMore} messages={state.conversation.getMessages()} />
       <SendMessageForm onSend={sendMessage} />
-    </React.Fragment>
+    </div>
   }
 }
 
diff --git a/client/src/components/Message.js b/client/src/components/Message.js
index f0aaba8..9054cd7 100644
--- a/client/src/components/Message.js
+++ b/client/src/components/Message.js
@@ -2,10 +2,13 @@
 import React from 'react'
 
 function Message(props) {
+    console.log("Message render")
+    console.log(props.message)
+
     return (
         <div className="message">
-            <div className="message-username">{props.username}</div>
-            <Typography className="message-text">{props.text}</Typography>
+            <div className="message-username">{props.message.author}</div>
+            <Typography className="message-text">{props.message.body}</Typography>
         </div>
     )
 }
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index 94947ca..b2fca5e 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -1,13 +1,21 @@
 import Message from './Message'
-import React from 'react'
+import React, { useEffect } from 'react'
 import { Box, Divider, Typography } from '@material-ui/core'
 import ConversationAvatar from './ConversationAvatar'
+const reverseMap = (arr, f) => arr.map((_, idx, arr) => f(arr[arr.length - 1 - idx ]));
 
 export default function MessageList(props) {
   const displayName = props.conversation.getDisplayName()
+  const messages = props.conversation.getMessages()
+  console.log("MessageList render " + messages.length)
+
+  useEffect(() => {
+    props.loadMore()
+  }, [props.conversation.getId()])
+
   return (
-    <div className="message-list">
-      <Box>
+    <React.Fragment>
+      <Box className="conversation-header">
         <Box style={{ display: 'inline-block', margin: 16, verticalAlign: 'middle' }}>
           <ConversationAvatar displayName={props.conversation.getDisplayNameNoFallback()} />
         </Box>
@@ -15,11 +23,13 @@
           <Typography variant="h6">{displayName}</Typography>
           <Typography variant="subtitle1">{props.conversation.getId()}</Typography>
         </Box>
+        <Divider orientation="horizontal" />
       </Box>
-      <Divider orientation="horizontal" />
-      {props.messages.map((message, index) =>
-        <Message key={index} username={message.senderId} text={message.body} />
-      )}
-    </div>
+      <div className="message-list">
+      <div className="message-list-inner">
+      {reverseMap(messages, (message) => <Message key={message.id} message={message} />)}
+      </div>
+      </div>
+    </React.Fragment>
   )
 }
\ No newline at end of file
diff --git a/client/src/components/SendMessageForm.js b/client/src/components/SendMessageForm.js
index fb06d49..82ee7af 100644
--- a/client/src/components/SendMessageForm.js
+++ b/client/src/components/SendMessageForm.js
@@ -7,7 +7,8 @@
 
 const useStyles = makeStyles((theme) => ({
   root: {
-    margin: 16,
+    marginLeft: 16,
+    marginRight: 16,
     padding: '2px 4px',
     display: 'flex',
     alignItems: 'center',
diff --git a/client/src/index.scss b/client/src/index.scss
index 970825f..ab30741 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -30,7 +30,17 @@
       "n m"
       "r m"
       "r m"
-      "r s";
+      "r m";
+}
+
+.messenger {
+  grid-area: m;
+  display: grid;
+  grid-template-rows: 73px 1fr 72px;
+  grid-template-areas:
+      "h"
+      "m"
+      "i";
 }
 
 .MuiContainer-root {
@@ -53,12 +63,26 @@
   grid-area: r;
 }
 
+.conversation-header {
+  grid-area: h;
+}
+
 .message-list {
   grid-area: m;
+  position: relative;
+}
+
+.message-list-inner {
+  max-height: 100%;
+  position: absolute;
+  overflow-y: auto;
+  width: 100%;
+  bottom: 0;
+  padding: 24px;
 }
 
 .send-message-form {
-  grid-area: s;
+  grid-area: i;
 }
 
 /* REST OF CSS */
@@ -88,83 +112,6 @@
   text-overflow: ellipsis;
 }
 
-/*
-.rooms-list {
-  box-sizing: border-box;
-  padding: 10px;
-  background-color: var(--main-color);
-  overflow: scroll;
-  height: 100%;
-}
-
-.rooms-list ul {
-  list-style-type: none;
-  padding: 0;
-  overflow: scoll;
-}
-
-.rooms-list li {
-  margin: 10px 0;
-}
-
-.rooms-list h3 {
-  margin: 5px 0;
-  color: var(--secondary-color);
-}
-
-.rooms-list .room a {
-  color: var(--secondary-text-color);
-  font-weight: 600;
-  text-decoration: none;
-
-}
-
-.rooms-list .room.active a {
-  color: var(--secondary-color);
-}
-*/
-/*
-.new-room-form {
-  padding: 0 5px;
-  background: var(--secondary-color);
-  color: var(--main-text-color);
-}
-
-.new-room-form form {
-  height: 100%;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.new-room-form input {
-  width: 135px;
-  background: var(--secondary-color);
-}
-
-.new-room-form button {
-  background: var(--secondary-color);
-  color: var(--main-text-color);
-  border: 0;
-}
-
-.new-room-form input::placeholder {
-  color: var(--main-text-color);
-  font-weight: 200;
-}
-
-.new-room-form input:focus {
-  outline-width: 0;
-}
-
-.new-room-form input {
-  border: 0;
-}
-
-.new-room-form button {
-  border: 0;
-}*/
-
 .list-placeholder {
   margin: 32px auto;
   width: 256px;
@@ -197,50 +144,8 @@
 .send-message-card {
   border-radius: 8px;
   margin: 16px;
-  /*padding: 8px;*/
-}
-/*
-.message-list {
-  box-sizing: border-box;
-  padding-left: 6px;
-  width: 100%;
-  height: 100%;
-  overflow: scroll;
-  background: var(--third-color);
 }
 
-.message-list .join-room {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 100%;
-  font-size: 34px;
-  font-weight: 300;
-}*/
-/*
-.send-message-form {
-  background: var(--send-message-form);
-  display: flex;
-}
-
-.send-message-form input {
-  width: 100%;
-  padding: 15px 10px;
-  margin: 0;
-  border-style: none;
-  background: var(--send-message-form);
-  font-weight: 200;
-
-}
-
-.send-message-form input:focus {
-  outline-width: 0;
-}
-
-.send-message-form input::placeholder {
-  color: var(--main-text-color);
-}
-*/
 .help-text {
   position: absolute;
   top: 10px;
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/messenger.jsx
index 3fef47b..5b40718 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/messenger.jsx
@@ -1,14 +1,10 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import Header from '../components/Header'
-import ContactList from '../components/ContactList'
-import MessageList from '../components/MessageList'
-import SendMessageForm from '../components/SendMessageForm'
 import NewContactForm from '../components/NewContactForm'
 
 //import Sound from 'react-sound';
 import io from "socket.io-client";
 import ConversationList from '../components/ConversationList';
-import CircularProgress from '@material-ui/core/CircularProgress';
 //const socket = io.connect('http://localhost:3000');
 import authManager from '../AuthManager'
 import Conversation from '../../../model/Conversation'
@@ -16,46 +12,26 @@
 import ConversationView from '../components/ConversationView';
 import AddContactPage from './addContactPage.jsx';
 import LoadingPage from '../components/loading';
+import { useParams } from 'react-router';
 
-class JamiMessenger extends React.Component {
+const JamiMessenger = (props) => {
+  const [conversations, setConversations] = useState(undefined)
+  const [searchResult, setSearchResults] = useState(undefined)
 
-  constructor(props) {
-    super(props)
+  const params = useParams()
+  const accountId = props.accountId || params.accountId
+  const conversationId = props.conversationId || params.conversationId
+  const contactId = props.contactId || params.contactId
 
-    this.state = {
-      conversations: undefined,
-      messages: [],
-      sound: false
-    }
-
+      //this.socket = socketIOClient(ENDPOINT);
     /*socket.on('connect', () => {
       console.log("Success !")
     })*/
-
-
-    //this.socket = socketIOClient(ENDPOINT);
-    //this.socket.on("FromAPI", data => {
+        //this.socket.on("FromAPI", data => {
     //  this.setState({
     //    messages: [...this.state.messages, data]
     //  })
     //});
-    this.sendMessage = this.sendMessage.bind(this)
-    this.handleSearch = this.handleSearch.bind(this)
-    this.controller = new AbortController()
-  }
-
-  componentDidMount() {
-    const accountId = this.props.accountId || this.props.match.params.accountId
-    const conversationId = this.props.conversationId || this.props.match.params.conversationId
-
-    if (this.req === undefined) {
-      this.req = authManager.fetch(`/api/accounts/${accountId}/conversations`, {signal: this.controller.signal})
-        .then(res => res.json())
-        .then(result => {
-          console.log(result)
-          this.setState({conversations:  Object.values(result).map(c => Conversation.from(accountId, c))})
-        })
-    }
     /*socket.on('receivedMessage', (data) => {
       const message = {
         senderId: '65f6674b26e5af6ed0b4e92a13b80ff4bbfdf1e8',
@@ -66,17 +42,29 @@
         sound: true
       })
     });*/
-  }
+  useEffect(() => {
+    console.log("io.connect")
+    const socket = io()
+    socket.on('receivedMessage', (data) => {
+      console.log("receivedMessage")
+      console.log(data)
+      conversation.addMessage(data)
+    })
+    return () => socket.disconnect()
+  })
 
-  componentWillUnmount(){
-    this.controller.abort()
-    this.req = undefined
-  }
+  useEffect(() => {
+    const controller = new AbortController()
+    authManager.fetch(`/api/accounts/${accountId}/conversations`, {signal: controller.signal})
+    .then(res => res.json())
+    .then(result => {
+      console.log(result)
+      setConversations(Object.values(result).map(c => Conversation.from(accountId, c)))
+    })
+    return () => controller.abort()
+  }, [accountId])
 
-  handleSearch(query) {
-    const accountId = this.props.accountId || this.props.match.params.accountId
-    const conversationId = this.props.conversationId || this.props.match.params.conversationId
-
+  const handleSearch = (query) => {
     authManager.fetch(`/api/accounts/${accountId}/ns/name/${query}`)
     .then(response => {
       if (response.status === 200) {
@@ -88,54 +76,26 @@
       console.log(response)
       const contact = new Contact(response.address)
       contact.setRegisteredName(response.name)
-      this.setState({searchResult: contact ? Conversation.fromSingleContact(accountId, contact) : undefined})
+      setSearchResults(contact ? Conversation.fromSingleContact(accountId, contact) : undefined)
     }).catch(e => {
-      this.setState({searchResult: e})
+      setSearchResults(undefined)
     })
   }
 
-  sendMessage(text) {
-    var data = {
-      senderId: 'Me',
-      destinationId: '65f6674b26e5af6ed0b4e92a13b80ff4bbfdf1e8',
-      text: text
-    }
-    //socket.emit("SendMessage", data);
-    console.log(data.text);
-    this.setState({
-      messages: [...this.state.messages, data],
-      sound: false
-    })
-  }
-  render() {
-    const accountId = this.props.accountId || this.props.match.params.accountId
-    const conversationId = this.props.conversationId || this.props.match.params.conversationId
-    const contactId = this.props.contactId || this.props.match.params.contactId
+  console.log("JamiMessenger render " + conversationId)
+  console.log(props)
 
-    console.log("JamiMessenger render " + conversationId)
-    console.log(this.props)
-    console.log(this.state)
-
-    return (
-      <div className="app" >
-        <Header />
-        {this.state.conversations ?
-          <ConversationList search={this.state.searchResult} conversations={this.state.conversations} accountId={accountId} /> :
-          <div className="rooms-list"><LoadingPage /></div>}
-        <NewContactForm onChange={query => this.handleSearch(query)} />
-        {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
-        {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
-        {this.state.sound && <Sound
-          url="stairs.mp3" /*https://notificationsounds.com/message-tones/stairs-567*/
-          playStatus={Sound.status.PLAYING}
-          playFromPosition={0 /* in milliseconds */}
-          onLoading={this.handleSongLoading}
-          onPlaying={this.handleSongPlaying}
-          onFinishedPlaying={this.handleSongFinishedPlaying}
-        />}
-      </div>
-    )
-  }
+  return (
+    <div className="app" >
+      <Header />
+      {conversations ?
+        <ConversationList search={searchResult} conversations={conversations} accountId={accountId} /> :
+        <div className="rooms-list"><LoadingPage /></div>}
+      <NewContactForm onChange={handleSearch} />
+      {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
+      {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
+    </div>
+  )
 }
 
 export default JamiMessenger
\ No newline at end of file
diff --git a/client/src/pages/serverConfiguration.jsx b/client/src/pages/serverConfiguration.jsx
new file mode 100644
index 0000000..ba99fe6
--- /dev/null
+++ b/client/src/pages/serverConfiguration.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import Header from '../components/Header'
+import AccountPreferences from '../components/AccountPreferences'
+import Container from '@material-ui/core/Container';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import authManager from '../AuthManager'
+import Account from '../../../model/Account'
+
+const ServerOverview = (props) => {
+
+    this.accountId = props.accountId || props.match.params.accountId
+    this.state = { loaded: false, account: props.account }
+    this.req = undefined
+
+  componentDidMount() {
+    this.controller = new AbortController()
+    if (this.req === undefined) {
+      this.req = authManager.fetch(`/api/serverConfig`, {signal: this.controller.signal})
+        .then(res => res.json())
+        .then(result => {
+          console.log(result)
+          this.setState({loaded: true, account: Account.from(result)})
+        })
+    }
+  }
+
+  componentWillUnmount() {
+    this.controller.abort()
+    this.req = undefined
+  }
+
+  return (
+    <Container maxWidth="sm" className="app" >
+      <Header />
+      {this.state.loaded ? <AccountPreferences account={this.state.account} /> : <CircularProgress />}
+    </Container>
+  )
+}
+
+export default ServerOverview;
\ No newline at end of file
diff --git a/client/webpack.config.js b/client/webpack.config.js
index 0b67ce9..0e96882 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -1,16 +1,17 @@
 'use strict'
 
-import dotenv from 'dotenv'
-dotenv.config({ path: resolve(import.meta.url, '..', '.env') })
-
-import { resolve } from 'path'
-import HtmlWebpackPlugin from 'html-webpack-plugin'
-import CopyWebpackPlugin from 'copy-webpack-plugin'
-
 import { fileURLToPath } from 'url';
 import { dirname } from 'path';
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
+
+import dotenv from 'dotenv'
+console.log(resolve(__dirname, '..', '.env'))
+dotenv.config({ path: resolve(__dirname, '..', '.env') })
+
+import { resolve } from 'path'
+import HtmlWebpackPlugin from 'html-webpack-plugin'
+import CopyWebpackPlugin from 'copy-webpack-plugin'
 const mode = process.env.NODE_ENV || 'development'
 
 let entry = [resolve(__dirname, 'src', 'index.js')]