set styles for messages
Change-Id: I8b86255a55a745a305f953f77122a98970de0958
diff --git a/client/package.json b/client/package.json
index 885e6ef..b60b40e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -5,6 +5,8 @@
"private": true,
"dependencies": {
"@babel/runtime": "^7.17.9",
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.2",
"@mui/lab": "^5.0.0-alpha.79",
"@mui/material": "^5.6.3",
@@ -12,6 +14,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
+ "dayjs": "^1.11.5",
"emoji-picker-react": "^3.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/client/src/components/ConversationView.js b/client/src/components/ConversationView.js
index d0ab227..075e96a 100644
--- a/client/src/components/ConversationView.js
+++ b/client/src/components/ConversationView.js
@@ -5,9 +5,11 @@
import Conversation from '../../../model/Conversation';
import LoadingPage from './loading';
import io from "socket.io-client";
+import { Box, Stack, Typography } from '@mui/material';
+import ConversationAvatar from './ConversationAvatar';
const ConversationView = props => {
- const [loadingMesages, setLoadingMesages] = useState(false)
+ const [loadingMessages, setLoadingMessages] = useState(false)
const [socket, setSocket] = useState(undefined)
const [state, setState] = useState({
loaded: false,
@@ -65,7 +67,7 @@
}, [state.conversation ? state.conversation.getId() : "", socket])
useEffect(() => {
- if (!loadingMesages || !state.conversation)
+ if (!loadingMessages || !state.conversation)
return
console.log(`Load more messages`)
const controller = new AbortController()
@@ -73,7 +75,7 @@
.then(res => res.json())
.then(messages => {
console.log(messages)
- setLoadingMesages(false)
+ setLoadingMessages(false)
setState(state => {
if (state.conversation)
state.conversation.addLoadedMessages(messages)
@@ -81,7 +83,7 @@
})
}).catch(e => console.log(e))
return () => controller.abort()
- }, [state, loadingMesages])
+ }, [state, loadingMessages])
const sendMessage = (message) => {
authManager.fetch(`/api/accounts/${props.accountId}/conversations/${props.conversationId}`, {
@@ -98,12 +100,35 @@
return <LoadingPage />
} else if (state.error === true) {
return <div>Error loding {props.conversationId}</div>
- } else {
- return <div className="messenger">
- <MessageList conversation={state.conversation} loading={loadingMesages} loadMore={() => setLoadingMesages(true)} messages={state.conversation.getMessages()} />
- <SendMessageForm onSend={sendMessage} />
- </div>
}
+
+ return (
+ <Stack
+ flexGrow={1}
+ height="100%"
+ >
+ <Stack direction="row" flexGrow={0}>
+ <Box style={{ margin: 16, flexShrink: 0 }}>
+ <ConversationAvatar displayName={state.conversation.getDisplayNameNoFallback()} />
+ </Box>
+ <Box style={{ flex: "1 1 auto", overflow: 'hidden' }}>
+ <Typography className="title" variant="h6">{state.conversation.getDisplayName()}</Typography>
+ <Typography className="subtitle" variant="subtitle1" >{state.conversation.getId()}</Typography>
+ </Box>
+ </Stack>
+ <Stack flexGrow={1} overflow="auto">
+ <MessageList
+ conversationId={state.conversation.getId()}
+ loading={loadingMessages}
+ loadMore={() => setLoadingMessages(true)}
+ messages={state.conversation.getMessages()}
+ />
+ </Stack>
+ <Stack flexGrow={0}>
+ <SendMessageForm onSend={sendMessage} />
+ </Stack>
+ </Stack>
+ )
}
export default ConversationView
\ No newline at end of file
diff --git a/client/src/components/Message.js b/client/src/components/Message.js
index 7cfbbdc..b86c43f 100644
--- a/client/src/components/Message.js
+++ b/client/src/components/Message.js
@@ -1,27 +1,236 @@
-import { Typography } from '@mui/material'
-import { GroupOutlined } from '@mui/icons-material'
-import React from 'react'
-import ConversationAvatar from './ConversationAvatar'
+import { Box, Chip, Divider, Stack, Typography } from "@mui/material"
+import dayjs from "dayjs"
+import isToday from "dayjs/plugin/isToday"
+import isYesterday from "dayjs/plugin/isYesterday"
+import React from "react"
-function Message(props) {
- 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 ''
+dayjs.extend(isToday)
+dayjs.extend(isYesterday)
+
+export const MessageCall = (props) => {
+ return (
+ <Stack
+ alignItems="center"
+ >
+ "Appel"
+ </Stack>
+ )
}
-export default Message
\ No newline at end of file
+export const MessageInitial = (props) => {
+ return (
+ <Stack
+ alignItems="center"
+ >
+ "Le Swarm a été créé"
+ </Stack>
+ )
+}
+
+export const MessageDataTransfer = (props) => {
+ return (
+ <MessageBubble
+ backgroundColor={"#E5E5E5"}
+ position={props.position}
+ isFirstOfGroup={props.isFirstOfGroup}
+ isLastOfGroup={props.isLastOfGroup}
+ >
+ "data-transfer"
+ </MessageBubble>
+ )
+}
+
+export const MessageMember = (props) => {
+ return (
+ <Stack
+ alignItems="center"
+ >
+ <Chip
+ sx={{
+ width: "fit-content",
+ }}
+ label={`${props.message.author} s'est joint`}
+ />
+ </Stack>
+ )
+}
+
+export const MessageMerge = (props) => {
+ return (
+ <Stack
+ alignItems="center"
+ >
+ "merge"
+ </Stack>
+ )
+}
+
+export const MessageText = (props) => {
+ return (
+ <MessageBubble
+ backgroundColor={props.bubbleColor}
+ position={props.position}
+ isFirstOfGroup={props.isFirstOfGroup}
+ isLastOfGroup={props.isLastOfGroup}
+ >
+ <Typography variant="body1" color={props.textColor}>
+ {props.message.body}
+ </Typography>
+ </MessageBubble>
+ )
+}
+
+export const MessageDate = ({time}) => {
+ let textDate
+
+ if (time.isToday()) {
+ textDate = "Today"
+ }
+ else if (time.isYesterday()) {
+ textDate = "Yesterday"
+ }
+ else {
+ const date = time.date().toString().padStart(2,'0')
+ const month = (time.month()+1).toString().padStart(2,'0')
+ textDate = `${date}/${month}/${time.year()}`
+ }
+
+ return (
+ <Box marginTop="30px">
+ <Divider>
+ {textDate}
+ </Divider>
+ </Box>
+ )
+}
+
+export const MessageTime = ({time, hasDateOnTop}) => {
+ const hour = time.hour().toString().padStart(2,'0')
+ const minute = time.minute().toString().padStart(2,'0')
+ const textTime = `${hour}:${minute}`
+
+ return (
+ <Stack
+ direction="row"
+ justifyContent="center"
+ margin="30px"
+ marginTop={hasDateOnTop ? "20px" : "30px"}
+ >
+ <Typography
+ variant="caption"
+ color="#A7A7A7"
+ fontWeight={700}
+ >
+ {textTime}
+ </Typography>
+ </Stack>
+ )
+}
+
+export const MessageBubblesGroup = (props) => {
+
+ const isUser = true // should access user from the store
+ const position = isUser ? "end" : "start"
+ const bubbleColor = isUser ? "#005699" : "#E5E5E5"
+ const textColor = isUser ? "white" : "black"
+
+ return (
+ <Stack // Container for an entire row with a single group of bubbles
+ direction="row"
+ justifyContent={position}
+ >
+ <Stack // Container for a group of bubbles with the partipants informations
+ width="66.66%"
+ paddingTop="30px"
+ alignItems={position}
+ >
+ <ParticipantName
+ name={props.messages[0]?.author}
+ position={position}
+ />
+ <Stack // Container for the bubbles alone
+ spacing="6px"
+ alignItems={position}
+ direction="column-reverse"
+ >
+ {props.messages.map(
+ (message, index) => {
+ let Component
+ switch (message.type) {
+ case "text/plain":
+ Component = MessageText
+ break
+ case "application/data-transfer+json":
+ Component = MessageDataTransfer
+ break
+ }
+ return (
+ <Component // Container for a single bubble
+ key={message.id}
+ message={message}
+ textColor={textColor}
+ position={position}
+ bubbleColor={bubbleColor}
+ isFirstOfGroup={index == props.messages.length-1}
+ isLastOfGroup={index == 0}
+ />
+ )
+ }
+ )}
+ </Stack>
+ </Stack>
+ </Stack>
+ )
+}
+
+const MessageBubble = (props) => {
+ const largeRadius = "20px"
+ const smallRadius = "5px"
+ const radius = React.useMemo(() => {
+ if (props.position == "start") {
+ return {
+ borderStartStartRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+ borderStartEndRadius: largeRadius,
+ borderEndStartRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+ borderEndEndRadius: largeRadius,
+ }
+ }
+ return {
+ borderStartStartRadius: largeRadius,
+ borderStartEndRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+ borderEndStartRadius: largeRadius,
+ borderEndEndRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+ }
+ }, [props.isFirstOfGroup, props.isLastOfGroup, props.position])
+
+ return (
+ <Box
+ sx={{
+ width: "fit-content",
+ backgroundColor: props.backgroundColor,
+ padding: "16px",
+ ...radius,
+ }}
+ >
+ {props.children}
+ </Box>
+ )
+}
+
+const ParticipantName = (props) => {
+ return (
+ <Box
+ marginBottom="6px"
+ marginLeft="16px"
+ marginRight="16px"
+ >
+ <Typography
+ variant="caption"
+ color="#A7A7A7"
+ fontWeight={700}
+ >
+ {props.name}
+ </Typography>
+ </Box>
+ )
+}
\ No newline at end of file
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index 3ae1b1c..3f16e46 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -1,33 +1,157 @@
-import Message from './Message'
+import dayjs from "dayjs"
import React, { useEffect } from 'react'
-import { Box, Divider, Typography } from '@mui/material'
-import ConversationAvatar from './ConversationAvatar'
+import dayOfYear from 'dayjs/plugin/dayOfYear'
+import isBetween from 'dayjs/plugin/isBetween'
+import { Stack } from "@mui/system"
+import { MessageCall, MessageDate, MessageInitial, MessageMember, MessageBubblesGroup, MessageTime, MessageMerge } from "./Message"
+
+dayjs.extend(dayOfYear)
+dayjs.extend(isBetween)
export default function MessageList(props) {
- const displayName = props.conversation.getDisplayName()
- const messages = props.conversation.getMessages()
+ const messagesComponents = buildMessagesList(props.messages)
useEffect(() => {
if (!props.loading)
props.loadMore()
- }, [props.conversation.getId()])
+ }, [props.conversationId])
return (
- <React.Fragment>
- <Box className="conversation-header">
- <Box style={{ margin: 16, flexShrink: 0 }}>
- <ConversationAvatar displayName={props.conversation.getDisplayNameNoFallback()} />
- </Box>
- <Box style={{ flex: "1 1 auto", overflow: 'hidden' }}>
- <Typography className="title" variant="h6">{displayName}</Typography>
- <Typography className="subtitle" variant="subtitle1" >{props.conversation.getId()}</Typography>
- </Box>
- <Divider orientation="horizontal" />
- </Box>
- <div className="message-list">
- {messages.map((message) => <Message key={message.id} message={message} />)}
- <div style={{ border: "1px solid transparent" }}/>
- </div>
- </React.Fragment>
+ <Stack
+ marginLeft="16px"
+ marginRight="16px"
+ direction="column-reverse"
+ >
+ {messagesComponents?.map(
+ ({Component, id, props}) => <Component key={id} {...props}/>
+ )}
+ </Stack>
)
-}
\ No newline at end of file
+}
+
+const buildMessagesList = (messages) => {
+ if (messages.length == 0) {
+ return null;
+ }
+
+ const components = []
+ let lastTime = dayjs.unix(messages[0].timestamp)
+ let lastAuthor = messages[0].author
+ let messageBubblesGroup = []
+
+ const pushMessageBubblesGroup = () => {
+ if (messageBubblesGroup.length == 0) {
+ return
+ }
+ components.push({
+ id: `group-${messageBubblesGroup[0].id}`,
+ Component: MessageBubblesGroup,
+ props: { messages: messageBubblesGroup },
+ })
+ messageBubblesGroup = []
+ }
+
+ const pushMessageCall = (message) => {
+ components.push({
+ id: `call-${message.id}`,
+ Component: MessageCall,
+ props: { message },
+ })
+ }
+
+ const pushMessageMember = (message) => {
+ components.push({
+ id: `member-${message.id}`,
+ Component: MessageMember,
+ props: { message },
+ })
+ }
+
+ const pushMessageMerge = (message) => {
+ components.push({
+ id: `merge-${message.id}`,
+ Component: MessageMerge,
+ props: { message },
+ })
+ }
+
+ const pushMessageTime = (message, time, hasDateOnTop=false) => {
+ components.push({
+ id: `time-${message.id}`,
+ Component: MessageTime,
+ props: { time, hasDateOnTop },
+ })
+ }
+
+ const pushMessageDate = (message, time) => {
+ components.push({
+ id: `date-${message.id}`,
+ Component: MessageDate,
+ props: { time }
+ })
+ }
+
+ const pushMessageInitial = (message) => {
+ components.push({
+ id: `initial-${message.id}`,
+ Component: MessageInitial,
+ props: { message }
+ })
+ }
+
+ messages.forEach(message => { // most recent messages first
+ switch (message.type) {
+ case "text/plain":
+ case "application/data-transfer+json":
+ if (lastAuthor != message.author) {
+ pushMessageBubblesGroup()
+ }
+ messageBubblesGroup.push(message)
+ break
+ case "application/call-history+json":
+ pushMessageBubblesGroup()
+ pushMessageCall(message)
+ break
+ case "member":
+ pushMessageBubblesGroup()
+ pushMessageMember(message)
+ break
+ case "merge":
+ pushMessageBubblesGroup()
+ pushMessageMerge(message)
+ break
+ case "initial":
+ default:
+ break
+ }
+
+ const time = dayjs.unix(message.timestamp)
+ if (message.type == "initial") {
+ pushMessageBubblesGroup()
+ pushMessageTime(message, time, true)
+ pushMessageDate(message, time)
+ pushMessageInitial(message)
+ }
+ else {
+ if ( // If the date is different
+ lastTime?.year() != time.year()
+ || lastTime?.dayOfYear() != time.dayOfYear()
+ ) {
+ pushMessageBubblesGroup()
+ pushMessageTime(message, time, true)
+ pushMessageDate(message, time)
+ }
+ else if ( // If more than 5 minutes have passed since the last message
+ !lastTime.isBetween(time, time?.add(5, "minute"))
+ ) {
+ pushMessageBubblesGroup()
+ pushMessageTime(message, time)
+ }
+
+ lastTime = time
+ lastAuthor = message.author
+ }
+ })
+
+ return components
+}
diff --git a/client/src/components/SendMessageForm.js b/client/src/components/SendMessageForm.js
index f447f34..8377575 100644
--- a/client/src/components/SendMessageForm.js
+++ b/client/src/components/SendMessageForm.js
@@ -1,6 +1,6 @@
import React from 'react'
import makeStyles from '@mui/styles/makeStyles';
-import { IconButton, InputBase, Paper, Popper } from '@mui/material'
+import { Box, IconButton, InputBase, Paper, Popper } from '@mui/material'
import { Send, EmojiEmotionsRounded } from '@mui/icons-material'
import EmojiPicker from 'emoji-picker-react'
@@ -50,7 +50,7 @@
const id = open ? 'simple-popover' : undefined
return (
- <div className="send-message-form">
+ <Box >
<Paper component="form"
onSubmit={handleSubmit}
className={classes.root}>
@@ -91,6 +91,6 @@
<Send />
</IconButton>
</Paper>
- </div>
+ </Box>
);
}
diff --git a/client/src/index.scss b/client/src/index.scss
index 5f0427e..a9b9681 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -1,158 +1,16 @@
-:root {
- --main-color: #1f1f1f;
- --secondary-color: white;
- --third-color: #8b8b8b;
- --main-text-color: #3e5869;
- --secondary-text-color: #b0c7d6;
- --send-message-form: #f5f5f5;
- --scroll-bar-bg-color: #04000000;
-}
-
html,
body {
height: 100%;
margin: 0;
padding: 0;
- font-family: "Open Sans", sans-serif;
- font-weight: 200;
- color: #3e5869;
- --scroll-bar-bg-color: rgba(0,0,0,0.2);
-}
-
-#root {
- height: 100%;
-}
-
-.app {
- display: grid;
- height: 100%;
- grid-template-columns: 320px 1fr;
- grid-template-rows: 40px 50px 1fr;
- grid-template-areas:
- "h m"
- "n m"
- "r m";
-}
-
-.messenger {
- grid-area: m;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.conversation-header {
- grid-area: h;
- display: flex;
- align-items: center;
- padding-right: 8px;
-
- .title, .subtitle {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-
-.send-message-form {
- margin: 0 16px 16px 8px;
-}
-
-.MuiContainer-root {
- padding-bottom: 32px;
-}
-
-.login {
- background-color: var(--main-color);
}
.main-search {
grid-area: n;
}
-/* REST OF CSS */
-
.main-search-input {
padding: 8px;
background-color: var(--secondary-color);
width: 100%;
}
-
-.rooms-list {
- grid-area: r;
- overflow-y: scroll;
-
- .MuiListItemText-primary,
- .MuiListItemText-secondary {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- scrollbar-color: var(--scroll-bar-bg-color) transparent;
- scrollbar-width: thin;
-}
-::-webkit-scrollbar {
- -webkit-appearance: none;
- width: 8px;
-}
-::-webkit-scrollbar-thumb {
- background-color: var(--scroll-bar-bg-color);
- border-radius: 4px;
-}
-
-.list-placeholder {
- margin: 32px auto;
- width: 256px;
- text-align: center;
-
- .subtitle {
- color: #a0a0a0;
- }
-}
-
-.message-list {
- flex: 1;
- height: 100%;
- overflow: auto;
- display: flex;
- flex-direction: column-reverse;
- padding: 16px 32px;
-
- .conversation-event {
- margin: 8px auto;
-
- .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 {
- border-radius: 8px;
- margin: 16px;
-}
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/messenger.jsx
index 3833956..48edb5d 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/messenger.jsx
@@ -11,6 +11,7 @@
import AddContactPage from './addContactPage.jsx';
import LoadingPage from '../components/loading';
import { useParams } from 'react-router';
+import { Stack } from '@mui/material';
const Messenger = (props) => {
const [conversations, setConversations] = useState(undefined)
@@ -57,15 +58,30 @@
console.log("Messenger render")
return (
- <div className="app" >
- <Header />
- <NewContactForm onChange={setSearchQuery} />
- {conversations ?
- <ConversationList search={searchResult} conversations={conversations} accountId={accountId} /> :
- <div className="rooms-list"><LoadingPage /></div>}
- {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
- {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
- </div>
+ <Stack
+ direction="row"
+ height="100vh"
+ width="100vw"
+ >
+ <Stack
+ flexGrow={0}
+ flexShrink={0}
+ overflow="auto"
+ >
+ <Header />
+ <NewContactForm onChange={setSearchQuery} />
+ {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
+ {conversations ?
+ <ConversationList search={searchResult} conversations={conversations} accountId={accountId} /> :
+ <div className="rooms-list"><LoadingPage /></div>
+ }
+ </Stack>
+ <Stack
+ flexGrow={1}
+ >
+ {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
+ </Stack>
+ </Stack>
)
}