set styles for messages
Change-Id: I8b86255a55a745a305f953f77122a98970de0958
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>
);
}