improve conversation view
Change-Id: I63189d0b61d45e659ac7618a977282f7b4500753
diff --git a/JamiDaemon.js b/JamiDaemon.js
index 59d6b2b..764f1a1 100755
--- a/JamiDaemon.js
+++ b/JamiDaemon.js
@@ -29,7 +29,7 @@
"AccountsChanged": () => {
console.log("AccountsChanged")
const newAccounts = []
- this.stringVectToArr(this.dring.getAccountList()).forEach(accountId => {
+ JamiDaemon.vectToJs(this.dring.getAccountList()).forEach(accountId => {
for (const account in this.accounts) {
if (account.id === accountId) {
newAccounts.push(account)
@@ -37,8 +37,8 @@
}
}
newAccounts.push(new Account(accountId,
- this.mapToJs(this.dring.getAccountDetails(accountId)),
- this.mapToJs(this.dring.getVolatileAccountDetails(accountId))
+ JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)),
+ JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId))
))
})
this.accounts = newAccounts
@@ -99,8 +99,26 @@
},
"RegisteredNameFound": (accountId, state, address, name) => {
console.log(`RegisteredNameFound: ${accountId} ${state} ${address} ${name}`)
+ const account = this.getAccount(accountId)
+ if (!account) {
+ console.log(`Unknown account ${accountId}`)
+ return
+ }
+ if (state == 0) {
+ const contact = account.getContactFromCache(address)
+ contact.setRegisteredName(name)
+ }
+ let index = account.lookups.length - 1;
+ while (index >= 0) {
+ const lookup = account.lookups[index]
+ if ((lookup.address && lookup.address === address) || (lookup.name && lookup.name === name)) {
+ lookup.resolve({address, name, state})
+ account.lookups.splice(index, 1);
+ }
+ index -= 1;
+ }
},
- "conversationReady": (accountId, conversationId) => {
+ "ConversationReady": (accountId, conversationId) => {
console.log(`conversationReady: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -109,13 +127,13 @@
}
let conversation = account.getConversation(conversationId)
if (!conversation) {
- const members = this.dring.getConversationMembers(accountId, conversationId)
- console.log(members)
- conversation = new Conversation(conversationId, members)
+ const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, conversationId))
+ members.forEach(member => member.contact = account.getContactFromCache(member.uri))
+ conversation = new Conversation(conversationId, accountId, members)
account.addConversation(conversation)
}
},
- "conversationRemoved": (accountId, conversationId) => {
+ "ConversationRemoved": (accountId, conversationId) => {
console.log(`conversationRemoved: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -124,7 +142,7 @@
}
account.removeConversation(conversationId)
},
- "conversationLoaded": (accountId, conversationId) => {
+ "ConversationLoaded": (accountId, conversationId) => {
console.log(`conversationLoaded: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -132,7 +150,7 @@
return
}
},
- "messageReceived": (accountId, conversationId, message) => {
+ "MessageReceived": (accountId, conversationId, message) => {
console.log(`messageReceived: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -144,7 +162,7 @@
conversation.addMessage(message)
}
},
- "conversationRequestReceived": (accountId, conversationId, request) => {
+ "ConversationRequestReceived": (accountId, conversationId, request) => {
console.log(`conversationRequestReceived: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -152,7 +170,7 @@
return
}
},
- "conversationMemberEvent": (accountId, conversationId, member, event) => {
+ "ConversationMemberEvent": (accountId, conversationId, member, event) => {
console.log(`conversationMemberEvent: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -160,7 +178,7 @@
return
}
},
- "onConversationError": (accountId, conversationId, code, what) => {
+ "OnConversationError": (accountId, conversationId, code, what) => {
console.log(`onConversationError: ${accountId} ${conversationId}`)
const account = this.getAccount(accountId)
if (!account) {
@@ -169,11 +187,19 @@
}
}
})
- this.stringVectToArr(this.dring.getAccountList()).forEach(accountId => {
- this.accounts.push(new Account(accountId,
- this.mapToJs(this.dring.getAccountDetails(accountId)),
- this.mapToJs(this.dring.getVolatileAccountDetails(accountId))
- ))
+
+ JamiDaemon.vectToJs(this.dring.getAccountList()).forEach(accountId => {
+ const account = new Account(accountId,
+ JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)),
+ JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId))
+ )
+ JamiDaemon.vectToJs(this.dring.getConversations(accountId)).forEach(conversationId => {
+ const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, conversationId))
+ members.forEach(member => member.contact = account.getContactFromCache(member.uri))
+ const conversation = new Conversation(conversationId, accountId, members)
+ account.addConversation(conversation)
+ })
+ this.accounts.push(account)
})
}
@@ -194,103 +220,158 @@
return this.accounts
}
/*getAccountDetails(accountId) {
- return this.mapToJs(this.dring.getAccountDetails(accountId));
+ return this.mapToJs(this.dring.getAccountDetails(accountId))
}*/
setAccountDetails(accountId, details) {
- this.dring.setAccountDetails(accountId, mapToNative(details));
+ this.dring.setAccountDetails(accountId, mapToNative(details))
}
getAudioOutputDeviceList() {
- return this.stringVectToArr(this.dring.getAudioOutputDeviceList());
+ return JamiDaemon.vectToJs(this.dring.getAudioOutputDeviceList())
}
getVolume(deviceName) {
- return this.dring.getVolume(deviceName);
+ return this.dring.getVolume(deviceName)
}
setVolume(deviceName, volume) {
- return this.dring.setVolume(deviceName, volume);
+ return this.dring.setVolume(deviceName, volume)
+ }
+
+ lookupName(accountId, name) {
+ const p = new Promise((resolve, reject) => {
+ const account = this.getAccount(accountId)
+ if (!account) {
+ reject(new Error("Can't find account"))
+ } else {
+ account.lookups.push({name, resolve, reject})
+ }
+ })
+ this.dring.lookupName(accountId, "", name)
+ return p
+ }
+
+ lookupAddress(accountId, address) {
+ console.log(`lookupAddress ${accountId} ${address}`)
+ const p = new Promise((resolve, reject) => {
+ const account = this.getAccount(accountId)
+ if (!account) {
+ reject(new Error("Can't find account"))
+ } else {
+ account.lookups.push({address, resolve, reject})
+ }
+ })
+ this.dring.lookupAddress(accountId, "", address)
+ return p
}
stop() {
- this.dring.fini();
+ this.dring.fini()
}
+ addContact(accountId, contactId) {
+ this.dring.addContact(accountId, contactId)
+ const details = JamiDaemon.mapToJs(this.dring.getContactDetails(accountId, contactId))
+ if (details.conversationId) {
+ const account = this.getAccount(accountId)
+ if (account) {
+ let conversation = account.getConversation(details.conversationId)
+ if (!conversation) {
+ const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, details.conversationId))
+ members.forEach(member => member.contact = account.getContactFromCache(member.uri))
+ conversation = new Conversation(details.conversationId, accountId, members)
+ account.addConversation(conversation)
+ }
+ }
+ }
+ return details
+ }
+
+ //getContactDetails
+
// private
boolToStr(bool) {
- return bool ? "true" : "false";
+ return bool ? "true" : "false"
}
accountDetailsToNative(account) {
- const params = new this.dring.StringMap();
+ const params = new this.dring.StringMap()
if (account.managerUri)
- params.set("Account.managerUri", account.managerUri);
+ params.set("Account.managerUri", account.managerUri)
if (account.managerUsername)
- params.set("Account.managerUsername", account.managerUsername);
+ params.set("Account.managerUsername", account.managerUsername)
if (account.archivePassword) {
- params.set("Account.archivePassword", account.archivePassword);
+ params.set("Account.archivePassword", account.archivePassword)
} else {
- console.log("archivePassword required");
+ console.log("archivePassword required")
return;
}
if (account.alias)
- params.set("Account.alias", account.alias);
+ params.set("Account.alias", account.alias)
if (account.displayName)
- params.set("Account.displayName", account.displayName);
+ params.set("Account.displayName", account.displayName)
if (account.enable)
- params.set("Account.enable", this.boolToStr(account.enable));
+ params.set("Account.enable", this.boolToStr(account.enable))
if (account.autoAnswer)
- params.set("Account.autoAnswer", this.boolToStr(account.autoAnswer));
+ params.set("Account.autoAnswer", this.boolToStr(account.autoAnswer))
if (account.ringtonePath)
- params.set("Account.ringtonePath", account.ringtonePath);
+ params.set("Account.ringtonePath", account.ringtonePath)
if (account.ringtoneEnabled)
- params.set("Account.ringtoneEnabled", this.boolToStr(account.ringtoneEnabled));
+ params.set("Account.ringtoneEnabled", this.boolToStr(account.ringtoneEnabled))
if (account.videoEnabled)
- params.set("Account.videoEnabled", this.boolToStr(account.videoEnabled));
+ params.set("Account.videoEnabled", this.boolToStr(account.videoEnabled))
if (account.useragent) {
- params.set("Account.useragent", account.useragent);
- params.set("Account.hasCustomUserAgent", "TRUE");
+ params.set("Account.useragent", account.useragent)
+ params.set("Account.hasCustomUserAgent", "TRUE")
} else {
- params.set("Account.hasCustomUserAgent", "FALSE");
+ params.set("Account.hasCustomUserAgent", "FALSE")
}
if (account.audioPortMin)
- params.set("Account.audioPortMin", account.audioPortMin);
+ params.set("Account.audioPortMin", account.audioPortMin)
if (account.audioPortMax)
- params.set("Account.audioPortMax", account.audioPortMax);
+ params.set("Account.audioPortMax", account.audioPortMax)
if (account.videoPortMin)
- params.set("Account.videoPortMin", account.videoPortMin);
+ params.set("Account.videoPortMin", account.videoPortMin)
if (account.videoPortMax)
- params.set("Account.videoPortMax", account.videoPortMax);
+ params.set("Account.videoPortMax", account.videoPortMax)
if (account.localInterface)
- params.set("Account.localInterface", account.localInterface);
+ params.set("Account.localInterface", account.localInterface)
if (account.publishedSameAsLocal)
- params.set("Account.publishedSameAsLocal", this.boolToStr(account.publishedSameAsLocal));
+ params.set("Account.publishedSameAsLocal", this.boolToStr(account.publishedSameAsLocal))
if (account.localPort)
- params.set("Account.localPort", account.localPort);
+ params.set("Account.localPort", account.localPort)
if (account.publishedPort)
- params.set("Account.publishedPort", account.publishedPort);
+ params.set("Account.publishedPort", account.publishedPort)
if (account.publishedAddress)
- params.set("Account.publishedAddress", account.publishedAddress);
+ params.set("Account.publishedAddress", account.publishedAddress)
if (account.upnpEnabled)
- params.set("Account.upnpEnabled", this.boolToStr(account.upnpEnabled));
- return params;
+ params.set("Account.upnpEnabled", this.boolToStr(account.upnpEnabled))
+ return params
}
- stringVectToArr(stringvect) {
- const outputArr = [];
- for (let i = 0; i < stringvect.size(); i++)
- outputArr.push(stringvect.get(i));
- return outputArr;
+ static vectToJs(vect) {
+ const len = vect.size()
+ const outputArr = new Array(len)
+ for (let i = 0; i < len; i++)
+ outputArr[i] = vect.get(i)
+ return outputArr
}
- mapToJs(m) {
- const outputObj = {};
- this.stringVectToArr(m.keys())
- .forEach(k => outputObj[k] = m.get(k));
- return outputObj;
+ static mapToJs(m) {
+ const outputObj = {}
+ JamiDaemon.vectToJs(m.keys())
+ .forEach(k => outputObj[k] = m.get(k))
+ return outputObj
}
- mapToNative(map){
- const ret = new this.dring.StringMap();
- map.forEach((value, key) => ret.set(key, value));
- return ret;
+ static vectMapToJs(vectMap) {
+ const len = vectMap.size()
+ const outputArr = new Array(len)
+ for (let i = 0; i < len; i++)
+ outputArr[i] = JamiDaemon.mapToJs(vectMap.get(i))
+ return outputArr
}
+ mapToNative(map){
+ const ret = new this.dring.StringMap()
+ map.forEach((value, key) => ret.set(key, value))
+ return ret;
+ }
}
module.exports = JamiDaemon;
diff --git a/client/src/App.js b/client/src/App.js
index bf9cc15..10db90a 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -12,10 +12,12 @@
import { BrowserRouter as Router, Route, Switch, Link, Redirect } from 'react-router-dom';
-import SignInPage from "./pages/loginDialog.jsx";
+import SignInPage from "./pages/loginDialog.jsx"
import JamiMessenger from "./pages/messenger.jsx"
import AccountSettings from "./pages/accountSettings.jsx"
import AccountSelection from "./pages/accountSelection.jsx"
+import AddContactPage from "./pages/addContactPage.jsx"
+
import NotFoundPage from "./pages/404.jsx"
class App extends React.Component {
@@ -28,13 +30,18 @@
}
render() {
+ console.log("App render")
+ console.log(this.props)
+
return <React.Fragment>
<CssBaseline />
<Router>
<Switch>
<Route exact path="/"><Redirect to="/account" /></Route>
- <Route path="/account/:accountId" component={JamiMessenger} />
<Route path="/account/:accountId/settings" component={AccountSettings} />
+ <Route path="/account/:accountId/addContact/:contactId" component={JamiMessenger} />
+ <Route path="/account/:accountId/conversation/:conversationId" component={JamiMessenger} />
+ <Route path="/account/:accountId" component={JamiMessenger} />
<Route path="/account" component={AccountSelection} />
<Route component={NotFoundPage} />
</Switch>
diff --git a/client/src/components/ContactList.js b/client/src/components/ContactList.js
index 50b773d..ad75783 100644
--- a/client/src/components/ContactList.js
+++ b/client/src/components/ContactList.js
@@ -4,9 +4,11 @@
class ContactList extends React.Component {
render() {
return (
+ <div className="rooms-list">
<List>
</List>
+ </div>
)
}
}
diff --git a/client/src/components/ConversationList.js b/client/src/components/ConversationList.js
index 4620d77..e83b6c1 100644
--- a/client/src/components/ConversationList.js
+++ b/client/src/components/ConversationList.js
@@ -1,16 +1,35 @@
import List from '@material-ui/core/List'
import React from 'react'
import ConversationListItem from './ConversationListItem'
+import ListSubheader from '@material-ui/core/ListSubheader';
+import Conversation from '../../../model/Conversation';
+import GroupRoundedIcon from '@material-ui/icons/GroupRounded';
+import { Typography } from '@material-ui/core';
class ConversationList extends React.Component {
render() {
-
+ console.log("ConversationList render " + this.props.accountId)
+ console.log(this.props.conversations)
return (
- <List>
- {this.props.conversations.forEach(conversation => {
- <ConversationListItem conversation={conversation} />
- })}
- </List>
+ <div className="rooms-list">
+ <List>
+ {this.props.search instanceof Conversation &&
+ (<div>
+ <ListSubheader>Public directory</ListSubheader>
+ <ConversationListItem conversation={this.props.search} />
+ <ListSubheader>Conversations</ListSubheader>
+ </div>)}
+ {this.props.conversations.map(conversation =>
+ <ConversationListItem key={conversation.getId()} conversation={conversation} />
+ )}
+ {this.props.conversations.length === 0 && (
+ <div className="list-placeholder">
+ <GroupRoundedIcon color="disabled" fontSize="large" />
+ <Typography className="subtitle" variant="subtitle2">No conversation yet</Typography>
+ </div>
+ )}
+ </List>
+ </div>
)
}
}
diff --git a/client/src/components/ConversationListItem.js b/client/src/components/ConversationListItem.js
index 3a271fc..cc72c70 100644
--- a/client/src/components/ConversationListItem.js
+++ b/client/src/components/ConversationListItem.js
@@ -1,16 +1,34 @@
import { Avatar, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'
import React from 'react'
+import Conversation from '../../../model/Conversation'
+import { withRouter } from 'react-router-dom';
class ConversationListItem extends React.Component {
render() {
+ const conversation = this.props.conversation;
+ const pathId = this.props.match.params.conversationId || this.props.match.params.contactId
+ const isSelected = conversation.getDisplayUri() === pathId
+ console.log("ConversationListItem render " + conversation)
+ console.log(this.props)
- return (
- <ListItem alignItems="flex-start">
- <ListItemAvatar><Avatar>{this.props.conversation.getDisplayName()[0]}</Avatar></ListItemAvatar>
- <ListItemText primary={this.props.conversation.getDisplayName()} />
- </ListItem>
- )
+ const uri = conversation.getId() ? `conversation/${conversation.getId()}` : `addContact/${conversation.getFirstMember().contact.getUri()}`;
+ if (conversation instanceof Conversation) {
+ return (
+ <ListItem
+ button
+ alignItems="flex-start"
+ selected={isSelected}
+ style={{overflow:'hidden'}}
+ onClick={() => this.props.history.push(`/account/${conversation.getAccountId()}/${uri}`)}>
+ <ListItemAvatar><Avatar>{conversation.getDisplayName()[0].toUpperCase()}</Avatar></ListItemAvatar>
+ <ListItemText
+ style={{overflow:'hidden', textOverflow:'ellipsis'}}
+ primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
+ </ListItem>
+ )
+ } else
+ return null
}
}
-export default ConversationListItem
\ No newline at end of file
+export default withRouter(ConversationListItem)
\ No newline at end of file
diff --git a/client/src/components/ConversationView.js b/client/src/components/ConversationView.js
new file mode 100644
index 0000000..678aafe
--- /dev/null
+++ b/client/src/components/ConversationView.js
@@ -0,0 +1,84 @@
+import CircularProgress from '@material-ui/core/CircularProgress';
+import React from 'react';
+import MessageList from './MessageList';
+import SendMessageForm from './SendMessageForm';
+import authManager from '../AuthManager'
+import Conversation from '../../../model/Conversation';
+
+class ConversationView extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ loaded:false,
+ error: false,
+ messages:[]
+ }
+ }
+
+ componentDidMount() {
+ this.controller = new AbortController()
+ if (!this.state.loaded) {
+ authManager.fetch(`/api/accounts/${this.props.accountId}/conversations/${this.props.conversationId}`, {signal: this.controller.signal})
+ .then(res => res.json())
+ .then(result => {
+ console.log(result)
+ this.setState({
+ loaded: true,
+ conversation: Conversation.from(this.props.accountId, result)// result.map(account => Account.from(account)),
+ })
+ }, error => {
+ console.log(`get error ${error}`)
+ this.setState({
+ loaded: true,
+ error: true
+ })
+ })
+ }
+ }
+ componentDidUpdate(prevProps, prevState) {
+ console.log("componentDidUpdate " + this.props.conversationId)
+ if (this.props.conversationId !== prevProps.conversationId) {
+ if (this.state.loaded === true) {
+ this.setState({
+ loaded:false,
+ error: false,
+ messages:[]
+ })
+ }
+ this.controller = new AbortController()
+ authManager.fetch(`/api/accounts/${this.props.accountId}/conversations/${this.props.conversationId}`, {signal: this.controller.signal})
+ .then(res => res.json())
+ .then(result => {
+ console.log(result)
+ this.setState({
+ loaded: true,
+ conversation: Conversation.from(this.props.accountId, result)// result.map(account => Account.from(account)),
+ })
+ }, error => {
+ console.log(`get error ${error}`)
+ this.setState({
+ loaded: true,
+ error: true
+ })
+ })
+ }
+ }
+ componentWillUnmount() {
+ this.controller.abort()
+ }
+
+ render() {
+ if (this.state.loaded === false) {
+ return <CircularProgress />
+ } else if (this.state.error === true) {
+ return <div>Error loding {this.props.conversationId}</div>
+ } else {
+ return <React.Fragment>
+ <MessageList conversation={this.state.conversation} messages={this.state.messages} />
+ <SendMessageForm sendMessage={this.sendMessage} />
+ </React.Fragment>
+ }
+ }
+}
+
+export default ConversationView
\ No newline at end of file
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index 08bdbea..48bfb28 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -1,12 +1,22 @@
+import { Avatar, Box, Divider, Typography } from '@material-ui/core'
+import Paper from '@material-ui/core/Paper'
import React from 'react'
import Message from './Message'
-
class MessageList extends React.Component {
render() {
return (
<div className="message-list">
-
+ <Box>
+ <Box style={{display:'inline-block', margin: 16, verticalAlign:'middle'}}>
+ <Avatar>{this.props.conversation.getDisplayName()[0].toUpperCase()}</Avatar>
+ </Box>
+ <Box style={{display:'inline-block', verticalAlign:'middle'}}>
+ <Typography variant="h5">{this.props.conversation.getDisplayName()}</Typography>
+ <Typography variant="subtitle1">{this.props.conversation.getId()}</Typography>
+ </Box>
+ </Box>
+ <Divider orientation="horizontal" />
{
this.props.messages.map((message, index) => {
/*DUMMY_DATA.map((message, index) => {*/
diff --git a/client/src/components/NewContactForm.js b/client/src/components/NewContactForm.js
index ef590c5..c98c9b4 100644
--- a/client/src/components/NewContactForm.js
+++ b/client/src/components/NewContactForm.js
@@ -1,17 +1,51 @@
import React from 'react'
+import SearchIcon from '@material-ui/icons/Search';
+import InputBase from '@material-ui/core/InputBase';
+import InputAdornment from '@material-ui/core/InputAdornment';
class NewContactForm extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {value: ''}
+ this.controller = new AbortController()
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+
+ componentDidMount() {
+ this.controller = new AbortController()
+ }
+
+ componentWillUnmount() {
+ this.controller.abort()
+ this.req = undefined
+ }
+
+ handleChange(event) {
+ this.setState({value: event.target.value})
+ this.props.onChange(event.target.value)
+ }
+
+ handleSubmit(event) {
+ event.preventDefault()
+ if (this.props.onSubmit)
+ this.props.onSubmit(this.state.value)
+ }
+
render() {
return (
- <div className="new-room-form">
- <form>
- <input
- type="text"
- placeholder="Ajouter un contact"
- required />
- <button id="create-room-btn" type="submit">+</button>
- </form>
- </div>
+ <form className="main-search" onSubmit={this.handleSubmit} noValidate autoComplete="off">
+ <InputBase
+ className="main-search-input"
+ type="search"
+ placeholder="Ajouter un contact"
+ onChange={this.handleChange}
+ startAdornment={
+ <InputAdornment position="start">
+ <SearchIcon />
+ </InputAdornment>
+ } />
+ </form>
)
}
}
diff --git a/client/src/components/SendMessageForm.js b/client/src/components/SendMessageForm.js
index fd648e5..5270e9a 100644
--- a/client/src/components/SendMessageForm.js
+++ b/client/src/components/SendMessageForm.js
@@ -1,51 +1,51 @@
import React from 'react'
-import TextField from '@material-ui/core/TextField'
-//import InputEmoji from "react-input-emoji";
+import { makeStyles } from '@material-ui/core/styles';
+import { IconButton, InputBase, Paper } from '@material-ui/core'
+import SendIcon from '@material-ui/icons/Send';
-class SendMessageForm extends React.Component {
+const useStyles = makeStyles((theme) => ({
+ root: {
+ margin: 16,
+ padding: '2px 4px',
+ display: 'flex',
+ alignItems: 'center',
+ borderRadius: 8
+ },
+ input: {
+ marginLeft: theme.spacing(1),
+ flex: 1,
+ },
+ iconButton: {
+ padding: 10,
+ },
+ divider: {
+ height: 28,
+ margin: 4,
+ },
+ }));
- constructor() {
- super()
- this.state = {
- message: ''
- }
- this.handleChange = this.handleChange.bind(this)
- this.handleSubmit = this.handleSubmit.bind(this)
+ export default function SendMessageForm(props) {
+ const classes = useStyles();
+
+ const handleSubmit = e => {
+ e.preventDefault()
}
- handleChange(e) {
- this.setState({
- message: e
- })
- }
-
- handleSubmit(e) {
- //e.preventDefault()
- this.props.sendMessage(this.state.message)
- //this.props.sendMessage(this.state.message)
- this.setState({
- message: ''
- })
- }
-
- render() {
- return (
- <div
- //onSubmit={this.handleSubmit}
- className="send-message-form">
- <TextField
- disabled={this.props.disabled}
- onChange={this.handleChange}
- value={this.state.message}
- //cleanOnEnter
- //onEnter={this.handleSubmit}
- placeholder="Écris ton message et cliques sur Entrer"
- height="35"
- />
-
- </div>
- )
- }
+ return (
+ <div className="send-message-form">
+ <Paper component="form"
+ onSubmit={handleSubmit}
+ className="send-message-card"
+ className={classes.root}>
+ <InputBase
+ className={classes.input}
+ placeholder="Write something nice"
+ height="35"
+ />
+ <IconButton type="submit" className={classes.iconButton} aria-label="search">
+ <SendIcon />
+ </IconButton>
+ </Paper>
+ </div>
+ )
}
-
-export default SendMessageForm
\ No newline at end of file
diff --git a/client/src/index.ejs b/client/src/index.ejs
index f1cd060..aee5126 100644
--- a/client/src/index.ejs
+++ b/client/src/index.ejs
@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
diff --git a/client/src/index.scss b/client/src/index.scss
index 062ee6f..73ba105 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -13,7 +13,7 @@
padding: 0;
font-family: 'Open Sans', sans-serif;
font-weight: 200;
- color: #3e5869;
+ color: #3e5869;
}
#root {
@@ -24,15 +24,15 @@
display: grid;
height: 100%;
grid-template-columns: repeat(6, 1fr);
- grid-template-rows: 40px 1fr 1fr 1fr 1fr 1fr 60px;
- grid-template-areas:
+ grid-template-rows: 40px 40px 1fr 1fr 1fr 1fr 92px;
+ grid-template-areas:
"h h h h h h"
+ "n m m m m m"
"r m m m m m"
"r m m m m m"
"r m m m m m"
"r m m m m m"
- "r m m m m m"
- "n s s s s s";
+ "r s s s s s";
}
.login {
@@ -43,7 +43,7 @@
grid-area: h;
}
-.new-room-form {
+.main-search {
grid-area: n;
}
@@ -70,6 +70,23 @@
color: var(--send-message-form);
}
+.main-search-input {
+ padding: 8px;
+ background-color: var(--secondary-color);
+ width: 100%;
+}
+
+.rooms-list {
+ overflow-y: scroll;
+}
+.rooms-list .MuiListItemText-primary,
+.rooms-list .MuiListItemText-secondary
+{
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/*
.rooms-list {
box-sizing: border-box;
padding: 10px;
@@ -79,7 +96,7 @@
}
.rooms-list ul {
- list-style-type: none;
+ list-style-type: none;
padding: 0;
overflow: scoll;
}
@@ -97,13 +114,14 @@
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);
@@ -126,7 +144,7 @@
background: var(--secondary-color);
color: var(--main-text-color);
border: 0;
-}
+}
.new-room-form input::placeholder {
color: var(--main-text-color);
@@ -143,6 +161,16 @@
.new-room-form button {
border: 0;
+}*/
+
+.list-placeholder {
+ margin: 32px auto;
+ width: 256px;
+ text-align: center;
+}
+
+.list-placeholder .subtitle {
+ color:#a0a0a0;
}
.message {
@@ -164,6 +192,12 @@
border-radius: 8px;
}
+.send-message-card {
+ border-radius: 8px;
+ margin: 16px;
+ /*padding: 8px;*/
+}
+/*
.message-list {
box-sizing: border-box;
padding-left: 6px;
@@ -180,8 +214,8 @@
height: 100%;
font-size: 34px;
font-weight: 300;
-}
-
+}*/
+/*
.send-message-form {
background: var(--send-message-form);
display: flex;
@@ -204,7 +238,7 @@
.send-message-form input::placeholder {
color: var(--main-text-color);
}
-
+*/
.help-text {
position: absolute;
top: 10px;
diff --git a/client/src/pages/accountSettings.jsx b/client/src/pages/accountSettings.jsx
index d7f71cf..12fa43a 100644
--- a/client/src/pages/accountSettings.jsx
+++ b/client/src/pages/accountSettings.jsx
@@ -13,10 +13,10 @@
this.accountId = props.accountId || props.match.params.accountId
this.state = { loaded: false, account: props.account }
this.req = undefined
- this.controller = new AbortController()
}
componentDidMount() {
+ this.controller = new AbortController()
if (this.req === undefined) {
this.req = authManager.fetch(`/api/accounts/${this.accountId}`, {signal: this.controller.signal})
.then(res => res.json())
diff --git a/client/src/pages/addContactPage.jsx b/client/src/pages/addContactPage.jsx
new file mode 100644
index 0000000..ee57b69
--- /dev/null
+++ b/client/src/pages/addContactPage.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { useHistory } from "react-router-dom";
+
+import { Box, Container, Fab, Card, CardContent, Typography } from '@material-ui/core';
+import GroupAddRounded from '@material-ui/icons/GroupAddRounded';
+import { makeStyles } from '@material-ui/core/styles';
+import authManager from '../AuthManager'
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ '& > *': {
+ margin: theme.spacing(1),
+ },
+ },
+ extendedIcon: {
+ marginRight: theme.spacing(1),
+ },
+}))
+
+export default function AddContactPage(props) {
+ const classes = useStyles()
+ const history = useHistory();
+ const accountId = props.accountId || props.match.params.accountId
+ const contactId = props.contactId || props.match.params.contactId
+
+ const handleClick = async e => {
+ const response = await authManager.fetch(`/api/accounts/${accountId}/conversations`, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({members:[contactId]})
+ }).then(res => res.json())
+
+ console.log(response)
+ if (response.conversationId) {
+ history.push(`/account/${accountId}/conversation/${response.conversationId}`)
+ }
+ }
+
+ return (
+ <Container className='message-list'>
+ <Card variant='outlined' style={{ borderRadius: 16, maxWidth: 560, margin: "16px auto" }}>
+ <CardContent>
+ <Typography variant='h6'>Jami key ID</Typography>
+ <Typography variant='body1'>{contactId}</Typography>
+ <Box style={{textAlign: 'center', marginTop: 16}}>
+ <Fab variant='extended' color='primary' onClick={handleClick}>
+ <GroupAddRounded className={classes.extendedIcon} />
+ Add contact
+ </Fab>
+ </Box>
+ </CardContent>
+ </Card>
+ </Container>)
+}
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/messenger.jsx
index a91a04e..f5e5104 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/messenger.jsx
@@ -4,18 +4,23 @@
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'
+import Contact from '../../../model/Contact'
+import ConversationView from '../components/ConversationView';
+import AddContactPage from './addContactPage.jsx';
class JamiMessenger extends React.Component {
constructor(props) {
super(props)
- this.accountId = props.accountId || props.match.params.accountId
+
this.state = {
conversations: undefined,
messages: [],
@@ -34,16 +39,20 @@
// })
//});
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/${this.accountId}/conversations`, {signal: this.controller.signal})
+ this.req = authManager.fetch(`/api/accounts/${accountId}/conversations`, {signal: this.controller.signal})
.then(res => res.json())
.then(result => {
console.log(result)
- this.setState({conversations: result})
+ this.setState({conversations: Object.values(result).map(c => Conversation.from(accountId, c))})
})
}
/*socket.on('receivedMessage', (data) => {
@@ -63,6 +72,27 @@
this.req = undefined
}
+ handleSearch(query) {
+ const accountId = this.props.accountId || this.props.match.params.accountId
+ const conversationId = this.props.conversationId || this.props.match.params.conversationId
+
+ authManager.fetch(`/api/accounts/${accountId}/ns/name/${query}`)
+ .then(response => {
+ if (response.status === 200) {
+ return response.json()
+ } else {
+ throw new Error(response.status)
+ }
+ }).then(response => {
+ console.log(response)
+ const contact = new Contact(response.address)
+ contact.setRegisteredName(response.name)
+ this.setState({searchResult: contact ? Conversation.fromSingleContact(accountId, contact) : undefined})
+ }).catch(e => {
+ this.setState({searchResult: e})
+ })
+ }
+
sendMessage(text) {
var data = {
senderId: 'Me',
@@ -77,13 +107,23 @@
})
}
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(this.props)
+ console.log(this.state)
+
return (
<div className="app" >
<Header />
- {this.state.conversations ? <ConversationList conversations={this.state.conversations} /> : <CircularProgress />}
- <MessageList messages={this.state.messages} />
- <SendMessageForm sendMessage={this.sendMessage} />
- <NewContactForm />
+ {this.state.conversations ?
+ <ConversationList search={this.state.searchResult} conversations={this.state.conversations} accountId={accountId} /> :
+ <CircularProgress />}
+ <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}
diff --git a/model/Account.js b/model/Account.js
index 6b43f1c..1f70c33 100644
--- a/model/Account.js
+++ b/model/Account.js
@@ -8,6 +8,7 @@
this.contactCache = {}
this.contacts = {}
this.conversations = {}
+ this.lookups = []
}
static from(object) {
@@ -54,6 +55,9 @@
getConversationIds() {
return Object.keys(this.conversations)
}
+ getConversations() {
+ return this.conversations
+ }
getConversation(conversationId) {
return this.conversations[conversationId]
diff --git a/model/Contact.js b/model/Contact.js
index b21ddcd..9d00098 100644
--- a/model/Contact.js
+++ b/model/Contact.js
@@ -6,12 +6,17 @@
}
static from(object) {
- return new Contact(object.uri)
+ const contact = new Contact(object.uri)
+ if (object.registeredName)
+ contact.setRegisteredName(object.registeredName)
+ return contact
}
getUri() { return this.uri }
- getRegisteredName() { this.registeredName }
+ getRegisteredName() { return this.registeredName }
+
+ setRegisteredName(name) { this.registeredName = name }
getDisplayName() {
return this.displayName || this.getRegisteredName() || this.getUri()
@@ -19,7 +24,8 @@
getObject() {
return {
- uri: this.uri
+ uri: this.uri,
+ registeredName: this.registeredName
}
}
}
diff --git a/model/Conversation.js b/model/Conversation.js
index 8046219..22dc477 100644
--- a/model/Conversation.js
+++ b/model/Conversation.js
@@ -1,27 +1,43 @@
+const Contact = require('./Contact')
+
class Conversation {
- constructor(id, members) {
+ constructor(id, accountId, members) {
this.id = id
+ this.accountId = accountId
this.members = members || []
this.messages = []
}
- static from(object) {
- return new Conversation(object.id, object.members)
+ static from(accountId, object) {
+ return new Conversation(object.id, accountId, object.members.map(member => {
+ member.contact = Contact.from(member.contact)
+ return member
+ }))
+ }
+ static fromSingleContact(accountId, contact) {
+ return new Conversation(undefined, accountId, [{contact}])
}
getId() { return this.id }
+ getAccountId() { return this.accountId }
+
getDisplayName() {
if (this.members.length !== 0) {
- return this.members[0].getDisplayName()
+ return this.members[0].contact.getDisplayName()
}
return this.getDisplayUri()
}
- getObject() {
+ getObject(params) {
+ const members = params.memberFilter ? this.members.filter(params.memberFilter) : this.members
return {
id: this.id,
- members: this.members
+ members: members.map(member => {
+ const copiedMember = { role: member.role }//Object.assign({}, member);
+ copiedMember.contact = member.contact.getObject()
+ return copiedMember
+ })
}
}
@@ -30,7 +46,15 @@
}
getDisplayUri() {
- return this.getId()
+ return this.getId() || this.getFirstMember().contact.getUri()
+ }
+
+ getFirstMember() {
+ return this.members[0]
+ }
+
+ getMembers() {
+ return this.members
}
addMessage(message) {
diff --git a/routes/jami.js b/routes/jami.js
index 9ba97a7..5d50002 100644
--- a/routes/jami.js
+++ b/routes/jami.js
@@ -7,6 +7,7 @@
getRouter() {
const router = express.Router({mergeParams: true})
+ router.use(express.json());
// Accounts
router.get(['/accounts'], (req, res, next) => {
@@ -14,7 +15,10 @@
res.json(this.jami.getAccountList().map(account => account.getSummary()))
})
- router.get(['/accounts/:accountId'], (req, res, next) => {
+ const accountRouter = express.Router({mergeParams: true})
+ router.use('/accounts/:accountId', accountRouter)
+
+ accountRouter.get(['/'], (req, res, next) => {
console.log(`Get account ${req.params.accountId}`)
const account = this.jami.getAccount(req.params.accountId)
if (account)
@@ -24,7 +28,7 @@
})
// Contacts
- router.get(['/accounts/:accountId/contacts'], (req, res, next) => {
+ accountRouter.get(['/contacts'], (req, res, next) => {
console.log(`Get account ${req.params.accountId}`)
const account = this.jami.getAccount(req.params.accountId)
if (account)
@@ -34,24 +38,79 @@
})
// Conversations
- const conversationRouter = express.Router({mergeParams: true})
- conversationRouter.get('/', (req, res, next) => {
+ accountRouter.get('/conversations', (req, res, next) => {
console.log(`Get conversations for account ${req.params.accountId}`)
const account = this.jami.getAccount(req.params.accountId)
if (!account)
res.sendStatus(404)
- res.json(account.getConversationIds())
+ const conversations = account.getConversations()
+ res.json(Object.keys(conversations).map(conversationId => conversations[conversationId].getObject({
+ memberFilter: member => member.contact.getUri() !== account.getUri()
+ })))
+ //res.json(account.getConversations())
})
- conversationRouter.get('/:conversationId', (req, res, next) => {
+ accountRouter.post('/conversations', (req, res, next) => {
+ console.log(`Create conversations for account, contact ${req.params.accountId}`)
+ console.log(req.body)
+ const account = this.jami.getAccount(req.params.accountId)
+ if (!account)
+ res.sendStatus(404)
+ if (req.body.members.length === 1) {
+ const details = this.jami.addContact(req.params.accountId, req.body.members[0])
+ res.json(details)
+ } else
+ res.sendStatus(400)
+ })
+
+ accountRouter.get('/conversations/:conversationId', (req, res, next) => {
console.log(`Get conversation ${req.params.conversationId} for account ${req.params.accountId}`)
const account = this.jami.getAccount(req.params.accountId)
if (!account)
res.sendStatus(404)
- res.json(account.getConversation(req.params.conversationId))
+ const conversation = account.getConversation(req.params.conversationId)
+ if (!conversation)
+ res.sendStatus(404)
+ else {
+ res.json(conversation.getObject({
+ memberFilter: member => member.contact.getUri() !== account.getUri()
+ }))
+ }
})
- router.use('/accounts/:accountId/conversations', conversationRouter)
+ // Nameserver
+ const nsRouter = express.Router({mergeParams: true})
+ accountRouter.use('/ns', nsRouter)
+
+ nsRouter.get(['/name/:nameQuery'], (req, res, next) => {
+ console.log(`Name lookup ${req.params.nameQuery}`)
+ this.jami.lookupName(req.params.accountId, req.params.nameQuery)
+ .then(result => {
+ if (result.state == 0)
+ res.json(result)
+ else if (result.state == 1)
+ res.sendStatus(400)
+ else
+ res.sendStatus(404)
+ }).catch(e => {
+ res.sendStatus(404)
+ })
+ })
+ nsRouter.get(['/addr/:addrQuery'], (req, res, next) => {
+ console.log(`Address lookup ${req.params.addrQuery}`)
+ this.jami.lookupAddress(req.params.accountId, req.params.addrQuery)
+ .then(result => {
+ if (result.state == 0)
+ res.json(result)
+ else if (result.state == 1)
+ res.sendStatus(400)
+ else
+ res.sendStatus(404)
+ }).catch(e => {
+ res.sendStatus(404)
+ })
+ })
+
return router
}
}