Skip to content
15 changes: 3 additions & 12 deletions src/components/ChatWindow/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Message } from '@twilio/conversations/lib/message';
import MessageInfo from './MessageInfo/MessageInfo';
import MessageListScrollContainer from './MessageListScrollContainer/MessageListScrollContainer';
import TextMessage from './TextMessage/TextMessage';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';

interface MessageListProps {
messages: Message[];
}

const useStyles = makeStyles({
messageListContainer: {
padding: '0 1.2em 1em',
overflowY: 'auto',
flex: 1,
},
});

export default function MessageList({ messages }: MessageListProps) {
const classes = useStyles();
const { room } = useVideoContext();
const localParticipant = room!.localParticipant;

return (
<div className={classes.messageListContainer}>
<MessageListScrollContainer messages={messages}>
{messages.map((message, idx) => {
const time = message.dateCreated
.toLocaleTimeString('en-us', { hour: 'numeric', minute: 'numeric' })
Expand All @@ -39,6 +30,6 @@ export default function MessageList({ messages }: MessageListProps) {
</React.Fragment>
);
})}
</div>
</MessageListScrollContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* istanbul ignore file */
import React from 'react';
import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
import Button from '@material-ui/core/Button';
import clsx from 'clsx';
import { Message } from '@twilio/conversations/lib/message';
import throttle from 'lodash.throttle';
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';

const styles = createStyles({
messageListContainer: {
overflowY: 'auto',
flex: '1',
},
outerContainer: {
minHeight: 0,
flex: 1,
position: 'relative',
},
innerScrollContainer: {
height: '100%',
overflowY: 'auto',
padding: '0 1.2em 1em',
},
button: {
position: 'absolute',
bottom: '14px',
right: '2em',
zIndex: 100,
padding: '0.5em 0.9em',
visibility: 'hidden',
opacity: 0,
boxShadow: '0px 4px 16px rgba(18, 28, 45, 0.2)',
transition: 'all 0.5s ease',
},
showButton: {
visibility: 'visible',
opacity: 1,
bottom: '24px',
},
});
interface MessageListScrollContainerProps extends WithStyles<typeof styles> {
messages: Message[];
}
interface MessageListScrollContainerState {
isScrolledToBottom: boolean;
showButton: boolean;
messageNotificationCount: number;
}

/*
* This component is a scrollable container that wraps around the 'MessageList' component.
* The MessageList will ultimately grow taller than its container as it continues to receive
* new messages, and users will need to have the ability to scroll up and down the chat window.
* A "new message" button will be displayed when the user receives a new message, and is not scrolled
* to the bottom. This button will be hidden if the user clicks on it, or manually scrolls
* to the bottom. Otherwise, this component will auto-scroll to the bottom when a new message is
* received, and the user is already scrolled to the bottom.
*
* Note that this component is tested with Cypress only.
*/
Comment on lines +52 to +61
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timmydoza do you mind proofreading this? want to make sure it's not too big

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Not too big. Can't have too many comments 🙂

export class MessageListScrollContainer extends React.Component<
MessageListScrollContainerProps,
MessageListScrollContainerState
> {
chatThreadRef = React.createRef<HTMLDivElement>();
state = { isScrolledToBottom: true, showButton: false, messageNotificationCount: 0 };

scrollToBottom() {
const innerScrollContainerEl = this.chatThreadRef.current!;
innerScrollContainerEl.scrollTop = innerScrollContainerEl!.scrollHeight;
}

componentDidMount() {
this.scrollToBottom();
this.chatThreadRef.current!.addEventListener('scroll', this.handleScroll);
}

// this component updates as users send new messages:
componentDidUpdate(prevProps: MessageListScrollContainerProps, prevState: MessageListScrollContainerState) {
if (prevState.isScrolledToBottom) {
this.scrollToBottom();
} else if (this.props.messages.length !== prevProps.messages.length) {
const numberOfNewMessages = this.props.messages.length - prevProps.messages.length;

this.setState(previousState => ({
// if there's at least one new message, show the 'new message' button:
showButton: !previousState.isScrolledToBottom,
// if 'new message' button is visible,
// messageNotificationCount will be the number of previously unread messages + the number of new messages
// otherwise, messageNotificationCount is set to 1:
messageNotificationCount: previousState.showButton
? previousState.messageNotificationCount + numberOfNewMessages
: 1,
}));
}
}

handleScroll = throttle(() => {
const innerScrollContainerEl = this.chatThreadRef.current!;
const isScrolledToBottom =
innerScrollContainerEl.clientHeight + innerScrollContainerEl.scrollTop === innerScrollContainerEl!.scrollHeight;

this.setState(prevState => ({
isScrolledToBottom,
showButton: isScrolledToBottom ? false : prevState.showButton,
}));
}, 300);

handleClick = () => {
const innerScrollContainerEl = this.chatThreadRef.current!;

innerScrollContainerEl.scrollTo({ top: innerScrollContainerEl.scrollHeight, behavior: 'smooth' });

this.setState({ showButton: false });
};

componentWillUnmount() {
const innerScrollContainerEl = this.chatThreadRef.current!;

innerScrollContainerEl.removeEventListener('scroll', this.handleScroll);
}

render() {
const { classes } = this.props;

return (
<div className={classes.outerContainer}>
<div className={classes.innerScrollContainer} ref={this.chatThreadRef}>
<div className={classes.messageListContainer}>
{this.props.children}
<Button
className={clsx(classes.button, { [classes.showButton]: this.state.showButton })}
onClick={this.handleClick}
startIcon={<ArrowDownwardIcon />}
color="primary"
variant="contained"
>
{this.state.messageNotificationCount} new message
{this.state.messageNotificationCount > 1 && 's'}
</Button>
</div>
</div>
</div>
);
}
}

export default withStyles(styles)(MessageListScrollContainer);
Loading