Updated chat look and feel

Changed to simple entry list
This commit is contained in:
Michael H. Arieli 2018-11-24 18:55:27 -08:00
parent a1e4ab78cd
commit 24eddf083f
6 changed files with 279 additions and 127 deletions

View File

@ -1,14 +1,15 @@
import * as constants from '../constants.js'
import _ from 'underscore'
export function addMessage ({ userId, message, timestamp }) {
export function addMessage ({ userId, message, timestamp, image }) {
return {
type: constants.MESSAGE_ADD,
payload: {
id: _.uniqueId('chat'),
userId,
message,
timestamp
timestamp,
image
}
}
}

View File

@ -1,11 +1,12 @@
import PropTypes from 'prop-types'
import React from 'react'
import socket from '../socket.js'
import moment from 'moment'
export const MessagePropTypes = PropTypes.shape({
userId: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired
timestamp: PropTypes.string.isRequired,
image: PropTypes.string
})
export default class Chat extends React.PureComponent {
@ -16,6 +17,43 @@ export default class Chat extends React.PureComponent {
document.getElementById('chat').classList.remove('show')
document.querySelector('.toolbar .chat').classList.remove('on')
}
scrollToBottom = () => {
// this.chatScroll.scrollTop = this.chatScroll.scrollHeight
const duration = 300
const start = this.chatScroll.scrollTop
const end = this.chatScroll.scrollHeight
const change = end - start
const increment = 20
const easeInOut = (currentTime, start, change, duration) => {
currentTime /= duration / 2
if (currentTime < 1) {
return change / 2 * currentTime * currentTime + start
}
currentTime -= 1
return -change / 2 * (currentTime * (currentTime - 2) - 1) + start
}
const animate = elapsedTime => {
elapsedTime += increment
const position = easeInOut(elapsedTime, start, change, duration)
this.chatScroll.scrollTop = position
if (elapsedTime < duration) {
setTimeout(() => {
animate(elapsedTime)
}, increment)
}
}
animate(0)
}
componentDidMount () {
this.scrollToBottom()
}
componentDidUpdate () {
this.scrollToBottom()
}
render () {
const { messages } = this.props
return (
@ -28,23 +66,37 @@ export default class Chat extends React.PureComponent {
</div>
<div className="chat-title">Chat</div>
</div>
<div className="chat-content">
<div className="chat-content" ref={div => { this.chatScroll = div }}>
{messages.length ? (
messages.map((message, i) => (
<div key={i}
className={
message.userId === socket.id
? 'chat-bubble alt'
: 'chat-bubble'
}
>
<div className="txt">
<p className="name">{message.userId}</p>
<p className="message">{message.message}</p>
<span className="timestamp">{message.timestamp}</span>
<div key={i} className="chat-item">
<div className="chat-item-label" />
<div className="chat-item-icon">
{message.image ? (
<div className="profile-image-component
profile-image-component-circle">
<div className="profile-image-component-image">
<img src={message.image} />
</div>
</div>
) : (
<div className="profile-image-component
profile-image-component-circle">
<div className="profile-image-component-initials">
{message.userId.substr(0, 2).toUpperCase()}
</div>
</div>
)}
</div>
<div className="chat-item-details">
<div className="chat-item-date">
{moment(message.timestamp).fromNow()}
</div>
</div>
<div className="chat-item-content">
{message.message}
</div>
<div className="arrow" />
</div>
))
) : (

View File

@ -1,6 +1,5 @@
import PropTypes from 'prop-types'
import React from 'react'
import moment from 'moment'
import socket from '../socket.js'
export default class Input extends React.PureComponent {
@ -24,7 +23,7 @@ export default class Input extends React.PureComponent {
this.submit()
}
handleKeyPress = e => {
if (e.key === 'Enter') {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
this.submit()
}
@ -37,8 +36,21 @@ export default class Input extends React.PureComponent {
sendMessage(message)
const userId = socket.id
const timestamp = moment().format('ddd, D MMM HH:mm a')
const payload = { userId, message, timestamp }
const timestamp = new Date()
let image = null
// take snapshoot
try {
const video = document.getElementById(`video-${userId}`)
const canvas = document.createElement('canvas')
canvas.height = video.videoHeight
canvas.width = video.videoWidth
const avatar = canvas.getContext('2d')
avatar.drawImage(video, 0, 0, canvas.width, canvas.height)
image = canvas.toDataURL()
} catch (e) {}
const payload = { userId, message, timestamp, image }
socket.emit('new_message', payload)
}
this.setState({ message: '' })
@ -46,15 +58,18 @@ export default class Input extends React.PureComponent {
render () {
const { message } = this.state
return (
<form className="input" onSubmit={this.handleSubmit}>
<input
<form className="chat-footer" onSubmit={this.handleSubmit}>
<textarea
className="input"
onChange={this.handleChange}
onKeyPress={this.handleKeyPress}
placeholder="Enter your message..."
type="text"
value={message}
/>
<input type="submit" value="Send" />
<button type="submit" className="send">
<span className="material-icons">send</span>
</button>
</form>
)
}

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import classnames from 'classnames'
import { MediaStream } from '../window.js'
import socket from '../socket.js'
export const StreamPropType = PropTypes.shape({
mediaStream: PropTypes.instanceOf(MediaStream).isRequired,
@ -48,6 +49,7 @@ export default class Video extends React.PureComponent {
return (
<div className={className}>
<video
id={`video-${socket.id}`}
autoPlay
onClick={this.handleClick}
onLoadedMetadata={this.play}

View File

@ -26,7 +26,7 @@ describe('components/Input', () => {
let input
beforeEach(() => {
sendMessage.mockClear()
input = node.querySelector('input')
input = node.querySelector('textarea')
TestUtils.Simulate.change(input, {
target: { value: message }
})

View File

@ -1,26 +1,4 @@
.input {
position: absolute;
right: 10px;
bottom: 30px;
z-index: 2;
width: calc(100% - 30px);
input {
box-shadow: 0px 0px 5px black;
background-color: #333;
border: none;
color: #ccc;
padding: 0.5rem;
font-family: $font-monospace;
}
input[type="text"] {
width: 85%;
}
}
#chat {
background-color: #f2f2f2;
position: fixed;
top: 0;
bottom: 0;
@ -80,14 +58,14 @@
}
.chat-content {
background-color: #999;
background-color: #f2f2f2;
bottom: 0;
left: 0;
overflow-y: auto;
position: absolute;
right: 0;
top: 52px;
padding: 20px;
bottom: 52px;
}
.chat-empty {
@ -100,7 +78,6 @@
-ms-transform: translateY(-50%);
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.chat-empty-icon {
font-size: 88px;
@ -110,88 +87,193 @@
font-size: 15px;
font-weight: bold;
}
}
.chat-bubble {
max-width: 240px;
height: auto;
display: block;
background: #f5f5f5;
border-radius: 4px;
box-shadow: 0px 0px 5px black;
.chat-item {
background-color: #fff;
box-shadow: 0 1px 1.5px 0 rgba(0, 0, 0, 0.12), 0 1px 6px rgba(0, 0, 0, 0.12);
line-height: 20px;
min-height: 80px;
padding: 15px 15px 15px 80px;
position: relative;
margin: 0 0 25px;
border-top: 1px solid #e9e9e9;
&.alt {
margin: 0 0 25px 60px;
.chat-item-label {
display: none;
}
&:last-child {
margin-bottom: 80px;
}
.txt {
padding: 8px 55px 8px 14px;
.name {
font-weight: 600;
font-size: 12px;
margin: 0 0 4px;
color: #3498db;
span {
font-weight: normal;
color: #b3b3b3;
}
&.alt {
color: #2ecc71;
}
}
.message {
font-size: 12px;
margin: 0;
color: #2b2b2b;
}
.timestamp {
font-size: 11px;
.chat-item-icon {
height: 50px;
left: 15px;
position: absolute;
top: 15px;
width: 50px;
}
.chat-item-details {
float: right;
text-align: right;
min-width: 30px;
min-height: 30px;
padding: 0 0 0 10px;
.chat-item-date {
color: #888;
font-size: 12px;
}
}
.chat-item-content {
color: #363636;
font-size: 13px;
font-weight: 400;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
> {
a, span a {
color: #363636;
font-weight: 700;
text-decoration: none;
}
}
}
&.is-clickable {
cursor: pointer;
&:hover {
background-color: #ebf2fe;
}
}
&.is-labeled {
border-top: none;
margin-top: 40px;
.chat-item-label {
color: #888;
display: block;
font-size: 13px;
font-weight: 700;
left: 0;
line-height: 20px;
padding: 10px;
position: absolute;
top: -40px;
}
}
}
.chat-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 52px;
background: #fff;
box-shadow: 0 1px 1.5px 0 rgba(0, 0, 0, 0.12), 0 1px 6px rgba(0, 0, 0, 0.12);
border-top: 1px solid #e9e9e9;
.input {
height: 52px;
background: #fff;
border: none;
width: calc(100% - 52px);
position: absolute;
left: 0;
top: 0;
padding: 10px;
resize: none;
overflow: scroll;
font-weight: 300;
&:focus {
outline: none;
}
-ms-overflow-style: none;
overflow: -moz-scrollbars-none;
//gotta hide windows scrollbars
&::-webkit-scrollbar {
width: 0 !important
}
}
.send {
position: absolute;
height: 32px;
width: 32px;
border-radius: 50%;
border: 0;
background: #000;
color: #fff;
bottom: 8px;
right: 10px;
text-transform: uppercase; color: #999
}
}
right: 8px;
padding: 4px 6px 0;
.arrow {
position: absolute;
width: 0;
bottom:42px;
left: -16px;
height: 0;
&:after {
content: "";
position: absolute;
border: 0 solid transparent;
border-top: 9px solid #f5f5f5;
border-radius: 0 20px 0;
width: 15px;
height: 30px;
transform: rotate(145deg);
}
}
&.alt {
.arrow {
right: -2px;
bottom: 40px;
left: auto;
&:after {
transform: rotate(45deg) scaleY(-1);
&:focus {
outline: none;
}
&:hover {
cursor: pointer;
}
}
}
}
.profile-image-component {
width: 100%;
height: 100%;
overflow: hidden;
text-align: center;
background-color: #ccc;
color: #fff;
position: relative;
font-size: 0;
line-height: 0;
.profile-image-component-icon, .profile-image-component-initials {
left: 0;
position: absolute;
right: 0;
top: 50%;
-ms-transform: translateY(-50%);
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.profile-image-component-icon {
font-size: 22px;
font-weight: 400;
}
.profile-image-component-initials {
font-size: 16px;
font-weight: 500;
vertical-align: middle;
}
.profile-image-component-image {
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
top: 0;
width: 100%;
> img {
height: 100%;
}
}
&.profile-image-component-circle {
border-radius: 50%;
.profile-image-component-image {
border-radius: 50%;
}
}
}