add messaging

Change-Id: I9f7b5c73e25774751a2c55ea6c2575feca5ebfd5
diff --git a/JamiDaemon.js b/JamiDaemon.js
index 6eeaa2c..8c23c79 100755
--- a/JamiDaemon.js
+++ b/JamiDaemon.js
@@ -148,7 +148,6 @@
             },
             "ConversationLoaded": (id, accountId, conversationId, messages) => {
                 console.log(`conversationLoaded: ${accountId} ${conversationId}`)
-                console.log(messages)
                 const account = this.getAccount(accountId)
                 if (!account) {
                     console.log(`Unknown account ${accountId}`)
@@ -252,6 +251,12 @@
     getAccountList() {
         return this.accounts
     }
+    getConversation(accountId, conversationId) {
+        const account = this.getAccount(accountId)
+        if (account)
+            return account.getConversation(conversationId)
+        return null
+    }
     /*getAccountDetails(accountId) {
         return this.mapToJs(this.dring.getAccountDetails(accountId))
     }*/
diff --git a/app.js b/app.js
index b8c09f4..74c58b4 100644
--- a/app.js
+++ b/app.js
@@ -118,7 +118,15 @@
     // app.use(app.router)
     app.use(cors(corsOptions))
 
-    const jami = new JamiDaemon()
+    const jami = new JamiDaemon((account, conversation, message) => {
+        console.log("JamiDaemon onMessage")
+
+        if (conversation.listeners) {
+            Object.values(conversation.listeners).forEach(listener => {
+                listener.socket.emit('newMessage', message)
+            })
+        }
+    })
     const apiRouter = new JamiRestApi(jami).getRouter()
 
     /*
@@ -293,6 +301,24 @@
         console.log(`saving sid ${socket.id} in session ${session.id}`)
         session.socketId = socket.id
         session.save()
+
+        socket.on("conversation", (data) => {
+            console.log(`io conversation`)
+            console.log(data);
+            if (session.conversation) {
+                console.log(`disconnect from old conversation ${session.conversation.conversationId}`)
+                const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId)
+                delete conversation.listeners[socket.id]
+            }
+            session.conversation = { accountId: data.accountId, conversationId: data.conversationId }
+            const conversation = jami.getConversation(data.accountId, data.conversationId)
+            if (!conversation.listeners)
+                conversation.listeners = {}
+            conversation.listeners[socket.id] = {
+                socket, session
+            }
+            session.save()
+        })
     })
 
     return server
diff --git a/client/src/App.js b/client/src/App.js
index bce1842..b500052 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -32,10 +32,6 @@
       return () => authManager.deinit()
     }, []);
 
-    console.log("App render")
-    console.log(state)
-    console.log(location)
-
     if (!state.loaded) {
       return <LoadingPage />
     } else if (!state.auth.setupComplete) {
diff --git a/client/src/AuthManager.js b/client/src/AuthManager.js
index 46b4466..c256d51 100644
--- a/client/src/AuthManager.js
+++ b/client/src/AuthManager.js
@@ -52,7 +52,6 @@
                 this.authenticating = false
                 this.state.initialized = true
                 console.log("Init ended")
-                console.log(response)
                 if (response.status === 200) {
                     const jsonData = await response.json()
                     Object.assign(this.state, {
@@ -71,9 +70,6 @@
                 } else {
                     this.state.error = true
                 }
-                console.log("New auth state")
-                console.log(this.state)
-
                 if (this.onAuthChanged)
                     this.onAuthChanged(this.state)
             })
@@ -150,7 +146,6 @@
         }
         return fetch(url, init)
             .then(response => {
-                console.log(`Got status ${response.status}`)
                 if (response.status === 401) {
                     this.disconnect()
                     return this.fetch(url, init)
diff --git a/client/src/components/ConversationView.js b/client/src/components/ConversationView.js
index 6217905..5c5a936 100644
--- a/client/src/components/ConversationView.js
+++ b/client/src/components/ConversationView.js
@@ -1,12 +1,14 @@
-import CircularProgress from '@material-ui/core/CircularProgress';
 import React, { useEffect, useState } from 'react';
 import MessageList from './MessageList';
 import SendMessageForm from './SendMessageForm';
 import authManager from '../AuthManager'
 import Conversation from '../../../model/Conversation';
 import LoadingPage from './loading';
+import io from "socket.io-client";
 
 const ConversationView = props => {
+  const [loadingMesages, setLoadingMesages] = useState(false)
+  const [socket, setSocket] = useState(undefined)
   const [state, setState] = useState({
     loaded: false,
     error: false,
@@ -21,7 +23,7 @@
       console.log(result)
       setState({
         loaded: true,
-        conversation: Conversation.from(props.accountId, result)// result.map(account => Account.from(account)),
+        conversation: Conversation.from(props.accountId, result)
       })
     }, error => {
       console.log(`get error ${error}`)
@@ -33,6 +35,52 @@
     return () => controller.abort()
   }, [props.accountId, props.conversationId])
 
+  useEffect(() => {
+    console.log("io.connect")
+    const socket = io()
+    setSocket(socket)
+    return () => {
+      console.log("io.disconnect")
+      socket.disconnect()
+      setSocket(undefined)
+    }
+  }, [])
+
+  useEffect(() => {
+    if (!state.conversation)
+      return
+    console.log(`io set conversation ${state.conversation.getId()} `+ socket)
+    if (socket)
+      socket.emit('conversation', { accountId: state.conversation.getAccountId(), conversationId: state.conversation.getId() })
+    socket.off('newMessage')
+    socket.on('newMessage', (data) => {
+      console.log("newMessage")
+      console.log(data)
+      setState(state => {
+        if (state.conversation)
+          state.conversation.addMessage(data)
+          return {...state}
+      })
+    })
+  }, [state.conversation ? state.conversation.getId() : "", socket])
+
+  useEffect(() => {
+    if (!loadingMesages || !state.conversation)
+      return
+    console.log(`Load more messages`)
+    const controller = new AbortController()
+    authManager.fetch(`/api/accounts/${state.conversation.getAccountId()}/conversations/${state.conversation.getId()}/messages`, {signal: controller.signal})
+      .then(res => res.json())
+      .then(messages => {
+        console.log(messages)
+        setLoadingMesages(false)
+        if (state.conversation)
+            state.conversation.addLoadedMessages(messages)
+        setState(state)
+      }).catch(e => console.log(e))
+      return () => controller.abort()
+  }, [state, loadingMesages])
+
   const sendMessage = (message) => {
     authManager.fetch(`/api/accounts/${props.accountId}/conversations/${props.conversationId}`, {
       headers: {
@@ -44,24 +92,13 @@
     })
   }
 
-  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 <div className="messenger">
-      <MessageList conversation={state.conversation} loadMore={loadMore} messages={state.conversation.getMessages()} />
+      <MessageList conversation={state.conversation} loading={loadingMesages} loadMore={() => setLoadingMesages(true)} messages={state.conversation.getMessages()} />
       <SendMessageForm onSend={sendMessage} />
     </div>
   }
diff --git a/client/src/components/Message.js b/client/src/components/Message.js
index 9054cd7..f4b4f57 100644
--- a/client/src/components/Message.js
+++ b/client/src/components/Message.js
@@ -1,16 +1,27 @@
 import { Typography } from '@material-ui/core'
+import { GroupOutlined } from '@material-ui/icons'
 import React from 'react'
+import ConversationAvatar from './ConversationAvatar'
 
 function Message(props) {
-    console.log("Message render")
-    console.log(props.message)
-
-    return (
-        <div className="message">
-            <div className="message-username">{props.message.author}</div>
-            <Typography className="message-text">{props.message.body}</Typography>
-        </div>
-    )
+    const message = props.message
+    if (message.type == 'text/plain')
+        return (<div className="message">
+            <div className="message-avatar">
+                    <ConversationAvatar name="{message.author}" /></div>
+                    <Typography className="message-text">{message.body}</Typography>
+                </div>)
+    else if (message.type == 'contact')
+        return (<div className="contact-event">
+            <Typography className="message-text">Contact event</Typography>
+        </div>)
+    else if (message.type == 'initial')
+        return (<div className="conversation-event">
+            <Typography variant="h6" className="message-text" color="textSecondary">
+                <div className="inline-avatar"><GroupOutlined color="action" style={{ fontSize: 32 }} /></div>Conversation created
+                </Typography>
+        </div>)
+    else return ''
 }
 
 export default Message
\ No newline at end of file
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index b2fca5e..e73af6a 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -2,15 +2,14 @@
 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()
+    if (!props.loading)
+      props.loadMore()
   }, [props.conversation.getId()])
 
   return (
@@ -26,9 +25,8 @@
         <Divider orientation="horizontal" />
       </Box>
       <div className="message-list">
-      <div className="message-list-inner">
-      {reverseMap(messages, (message) => <Message key={message.id} message={message} />)}
-      </div>
+        {messages.map((message) => <Message key={message.id} message={message} />)}
+        <div style={{ border: "1px solid transparent" }}/>
       </div>
     </React.Fragment>
   )
diff --git a/client/src/index.scss b/client/src/index.scss
index ab30741..19e2639 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -1,17 +1,18 @@
 :root {
-  --main-color: #1F1F1F;
+  --main-color: #1f1f1f;
   --secondary-color: white;
   --third-color: #8b8b8b;
   --main-text-color: #3e5869;
   --secondary-text-color: #b0c7d6;
-  --send-message-form: #F5F5F5;
+  --send-message-form: #f5f5f5;
 }
 
-html, body {
+html,
+body {
   height: 100%;
   margin: 0;
   padding: 0;
-  font-family: 'Open Sans', sans-serif;
+  font-family: "Open Sans", sans-serif;
   font-weight: 200;
   color: #3e5869;
 }
@@ -26,21 +27,25 @@
   grid-template-columns: 320px 1fr;
   grid-template-rows: 40px 50px 1fr 1fr 92px;
   grid-template-areas:
-      "h m"
-      "n m"
-      "r m"
-      "r m"
-      "r m";
+    "h m"
+    "n m"
+    "r m"
+    "r m"
+    "r m";
 }
 
 .messenger {
   grid-area: m;
-  display: grid;
-  grid-template-rows: 73px 1fr 72px;
-  grid-template-areas:
-      "h"
-      "m"
-      "i";
+  display: flex;
+  flex-direction: column;
+}
+
+.conversation-header {
+  grid-area: h;
+}
+
+.send-message-form {
+  margin: 0 16px 16px 8px;
 }
 
 .MuiContainer-root {
@@ -51,51 +56,12 @@
   background-color: var(--main-color);
 }
 
-.header-section {
-  grid-area: h;
-}
-
 .main-search {
   grid-area: n;
 }
 
-.rooms-list {
-  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: i;
-}
-
 /* REST OF CSS */
 
-.header-section {
-  background-color: var(--main-color);
-
-}
-
-.simple-menu {
-  color: var(--send-message-form);
-}
-
 .main-search-input {
   padding: 8px;
   background-color: var(--secondary-color);
@@ -103,42 +69,67 @@
 }
 
 .rooms-list {
+  grid-area: r;
   overflow-y: scroll;
-}
-.rooms-list .MuiListItemText-primary,
-.rooms-list .MuiListItemText-secondary
-{
-  overflow: hidden;
-  text-overflow: ellipsis;
+
+  .MuiListItemText-primary,
+  .MuiListItemText-secondary {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
 }
 
 .list-placeholder {
   margin: 32px auto;
   width: 256px;
   text-align: center;
+
+  .subtitle {
+    color: #a0a0a0;
+  }
 }
 
-.list-placeholder .subtitle {
-  color:#a0a0a0;
-}
+.message-list {
+  flex: 1;
+  height: 100%;
+  overflow: auto;
+  display: flex;
+  flex-direction: column-reverse;
+  padding: 16px 32px;
 
-.message {
-  margin: 15px 0;
-}
+  .conversation-event {
+    margin: 8px auto;
 
-.message .message-username {
-  font-size: 11px;
-  font-weight: bold;
-  color: var(--secondary-color);
-  opacity: 0.9;
-  margin-bottom: 6px;
-}
-.message .message-text {
-  background: var(--third-color);
-  color: var(--secondary-color);
-  display: inline;
-  padding: 8px 16px;
-  border-radius: 16px;
+    .inline-avatar {
+      display: inline-block;
+      vertical-align: middle;
+      margin: 16px;
+    }
+  }
+
+  .message {
+    margin: 8px 0;
+
+    .message-avatar {
+      display: inline-block;
+      vertical-align: middle;
+    }
+    .message-username {
+      font-size: 11px;
+      font-weight: bold;
+      color: var(--secondary-color);
+      opacity: 0.9;
+      margin-bottom: 6px;
+    }
+    .message-text {
+      background: var(--third-color);
+      color: var(--secondary-color);
+      display: inline;
+      padding: 8px 16px;
+      border-radius: 16px;
+      margin: 8px;
+    }
+  }
 }
 
 .send-message-card {
diff --git a/client/src/pages/addContactPage.jsx b/client/src/pages/addContactPage.jsx
index ee57b69..a4c32d7 100644
--- a/client/src/pages/addContactPage.jsx
+++ b/client/src/pages/addContactPage.jsx
@@ -40,7 +40,7 @@
   }
 
   return (
-    <Container className='message-list'>
+    <Container className='messenger'>
       <Card variant='outlined' style={{ borderRadius: 16, maxWidth: 560, margin: "16px auto" }}>
         <CardContent>
           <Typography variant='h6'>Jami key ID</Typography>
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/messenger.jsx
index 5b40718..9b825f9 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/messenger.jsx
@@ -3,9 +3,7 @@
 import NewContactForm from '../components/NewContactForm'
 
 //import Sound from 'react-sound';
-import io from "socket.io-client";
 import ConversationList from '../components/ConversationList';
-//const socket = io.connect('http://localhost:3000');
 import authManager from '../AuthManager'
 import Conversation from '../../../model/Conversation'
 import Contact from '../../../model/Contact'
@@ -16,6 +14,7 @@
 
 const JamiMessenger = (props) => {
   const [conversations, setConversations] = useState(undefined)
+  const [searchQuery, setSearchQuery] = useState('')
   const [searchResult, setSearchResults] = useState(undefined)
 
   const params = useParams()
@@ -23,36 +22,6 @@
   const conversationId = props.conversationId || params.conversationId
   const contactId = props.contactId || params.contactId
 
-      //this.socket = socketIOClient(ENDPOINT);
-    /*socket.on('connect', () => {
-      console.log("Success !")
-    })*/
-        //this.socket.on("FromAPI", data => {
-    //  this.setState({
-    //    messages: [...this.state.messages, data]
-    //  })
-    //});
-    /*socket.on('receivedMessage', (data) => {
-      const message = {
-        senderId: '65f6674b26e5af6ed0b4e92a13b80ff4bbfdf1e8',
-        text: data
-      }
-      this.setState({
-        messages: [...this.state.messages, message],
-        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()
-  })
-
   useEffect(() => {
     const controller = new AbortController()
     authManager.fetch(`/api/accounts/${accountId}/conversations`, {signal: controller.signal})
@@ -64,8 +33,11 @@
     return () => controller.abort()
   }, [accountId])
 
-  const handleSearch = (query) => {
-    authManager.fetch(`/api/accounts/${accountId}/ns/name/${query}`)
+  useEffect(() => {
+    if (!searchQuery)
+      return
+    const controller = new AbortController()
+    authManager.fetch(`/api/accounts/${accountId}/ns/name/${searchQuery}`, {signal: controller.signal})
     .then(response => {
       if (response.status === 200) {
         return response.json()
@@ -80,18 +52,16 @@
     }).catch(e => {
       setSearchResults(undefined)
     })
-  }
-
-  console.log("JamiMessenger render " + conversationId)
-  console.log(props)
+    return () => controller.abort()
+  }, [accountId, searchQuery])
 
   return (
     <div className="app" >
       <Header />
+      <NewContactForm onChange={setSearchQuery} />
       {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>