improve conversation view
Change-Id: I63189d0b61d45e659ac7618a977282f7b4500753
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}