Feat (chat) add new message badge (#11987)

Change scroll to bottom when receive a new message with:
- scroll to bottom if scroll was at the bottom before getting a new message
- keep the scroll in his original position when the scroll position was not at the bottom
- scroll to bottom when open first time the chat
This commit is contained in:
bogdandarie 2022-08-26 09:21:41 +03:00 committed by GitHub
parent b9aeb19379
commit be1752c162
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 453 additions and 242 deletions

View File

@ -30,12 +30,18 @@
height: calc(100% - 70px);
}
#chat-conversation-container {
// extract message input height
height: calc(100% - 68px);
overflow: hidden;
position: relative;
}
#chatconversation {
box-sizing: border-box;
flex: 1;
font-size: 10pt;
// extract message input height
height: calc(100% - 68px);
height: 100%;
line-height: 20px;
overflow: auto;
padding: 16px;

View File

@ -97,6 +97,7 @@
"messageAccessibleTitleMe": "me says:",
"messageTo": "Private message to {{recipient}}",
"messagebox": "Type a message",
"newMessages": "new messages",
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat",

View File

@ -112,4 +112,4 @@ export const SET_IS_POLL_TAB_FOCUSED = 'SET_IS_POLL_TAB_FOCUSED';
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
* }
*/
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';

View File

@ -1,13 +1,13 @@
// @flow
import { Component } from 'react';
import { PureComponent } from 'react';
import { IMessage } from '../reducer';
export type Props = {
export interface Props {
/**
* The messages array to render.
*/
messages: Array<Object>
messages: IMessage[]
}
/**
@ -15,9 +15,9 @@ export type Props = {
*
* @augments PureComponent
*/
export default class AbstractMessageContainer<P: Props> extends PureComponent<P> {
export default class AbstractMessageContainer<P extends Props, S> extends Component<P, S> {
static defaultProps = {
messages: []
messages: [] as IMessage[]
};
/**
@ -29,8 +29,8 @@ export default class AbstractMessageContainer<P: Props> extends PureComponent<P>
*/
_getMessagesGroupedBySender() {
const messagesCount = this.props.messages.length;
const groups = [];
let currentGrouping = [];
const groups: IMessage[][] = [];
let currentGrouping: IMessage[] = [];
let currentGroupParticipantId;
for (let i = 0; i < messagesCount; i++) {

View File

@ -44,34 +44,11 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance.
this._onChatTabKeyDown = this._onChatTabKeyDown.bind(this);
this._onChatInputResize = this._onChatInputResize.bind(this);
this._onEscClick = this._onEscClick.bind(this);
this._onPollsTabKeyDown = this._onPollsTabKeyDown.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
}
/**
* Implements {@code Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._scrollMessageContainerToBottom(true);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._messages !== prevProps._messages) {
this._scrollMessageContainerToBottom(true);
} else if (this.props._isOpen && !prevProps._isOpen) {
this._scrollMessageContainerToBottom(false);
}
}
/**
* Implements React's {@link Component#render()}.
*
@ -98,19 +75,6 @@ class Chat extends AbstractChat<Props> {
);
}
_onChatInputResize: () => void;
/**
* Callback invoked when {@code ChatInput} changes height. Preserves
* displaying the latest message if it is scrolled to.
*
* @private
* @returns {void}
*/
_onChatInputResize() {
this._messageContainerRef.current.maybeUpdateBottomScroll();
}
_onChatTabKeyDown: (KeyboardEvent) => void;
/**
@ -172,7 +136,7 @@ class Chat extends AbstractChat<Props> {
if (_isPollsTabFocused) {
return (
<>
{_isPollsEnabled && this._renderTabs()}
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = 'polls-tab'
id = 'polls-panel'
@ -186,16 +150,16 @@ class Chat extends AbstractChat<Props> {
return (
<>
{_isPollsEnabled && this._renderTabs()}
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = 'chat-tab'
className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
id = 'chat-panel'
role = 'tabpanel'>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
messages = { this.props._messages } />
<MessageRecipient />
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this._onSendMessage } />
@ -222,8 +186,7 @@ class Chat extends AbstractChat<Props> {
aria-controls = 'chat-panel'
aria-label = { t('chat.tabs.chat') }
aria-selected = { !_isPollsTabFocused }
className = { `chat-tab ${
_isPollsTabFocused ? '' : 'chat-tab-focus'
className = { `chat-tab ${_isPollsTabFocused ? '' : 'chat-tab-focus'
}` }
id = 'chat-tab'
onClick = { this._onToggleChatTab }
@ -232,21 +195,20 @@ class Chat extends AbstractChat<Props> {
tabIndex = '0'>
<span
className = { 'chat-tab-title' }>
{t('chat.tabs.chat')}
{ t('chat.tabs.chat') }
</span>
{this.props._isPollsTabFocused
{ this.props._isPollsTabFocused
&& _nbUnreadMessages > 0 && (
<span className = { 'chat-tab-badge' }>
{_nbUnreadMessages}
{ _nbUnreadMessages }
</span>
)}
) }
</div>
<div
aria-controls = 'polls-panel'
aria-label = { t('chat.tabs.polls') }
aria-selected = { _isPollsTabFocused }
className = { `chat-tab ${
_isPollsTabFocused ? 'chat-tab-focus' : ''
className = { `chat-tab ${_isPollsTabFocused ? 'chat-tab-focus' : ''
}` }
id = 'polls-tab'
onClick = { this._onTogglePollsTab }
@ -254,33 +216,19 @@ class Chat extends AbstractChat<Props> {
role = 'tab'
tabIndex = '0'>
<span className = { 'chat-tab-title' }>
{t('chat.tabs.polls')}
{ t('chat.tabs.polls') }
</span>
{!_isPollsTabFocused
{ !_isPollsTabFocused
&& this.props._nbUnreadPolls > 0 && (
<span className = { 'chat-tab-badge' }>
{_nbUnreadPolls}
{ _nbUnreadPolls }
</span>
)}
) }
</div>
</div>
);
}
/**
* Scrolls the chat messages so the latest message is visible.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling
* animation.
* @private
* @returns {void}
*/
_scrollMessageContainerToBottom(withAnimation) {
if (this._messageContainerRef.current) {
this._messageContainerRef.current.scrollToBottom(withAnimation);
}
}
_onSendMessage: (string) => void;
_onToggleChat: () => void;
@ -295,7 +243,6 @@ class Chat extends AbstractChat<Props> {
}
_onTogglePollsTab: () => void;
_onToggleChatTab: () => void;
}
export default translate(connect(_mapStateToProps)(Chat));

View File

@ -1,5 +1,5 @@
/* eslint-disable lines-around-comment */
import React, { Component } from 'react';
import React, { Component, RefObject } from 'react';
import { WithTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
@ -31,17 +31,10 @@ interface Props extends WithTranslation {
*/
dispatch: Dispatch<any>,
/**
* Optional callback to invoke when the chat textarea has auto-resized to
* fit overflowing text.
*/
onResize?: Function,
/**
* Callback to invoke on message send.
*/
onSend: Function
}
/**
@ -66,7 +59,7 @@ type State = {
* @augments Component
*/
class ChatInput extends Component<Props, State> {
_textArea?: HTMLTextAreaElement;
_textArea?: RefObject<HTMLTextAreaElement>;
state = {
message: '',
@ -82,7 +75,7 @@ class ChatInput extends Component<Props, State> {
constructor(props: Props) {
super(props);
this._textArea = undefined;
this._textArea = React.createRef<HTMLTextAreaElement>();
// Bind event handlers so they are only bound once for every instance.
this._onDetectSubmit = this._onDetectSubmit.bind(this);
@ -90,7 +83,6 @@ class ChatInput extends Component<Props, State> {
this._onSmileySelect = this._onSmileySelect.bind(this);
this._onSubmitMessage = this._onSubmitMessage.bind(this);
this._toggleSmileysPanel = this._toggleSmileysPanel.bind(this);
this._setTextAreaRef = this._setTextAreaRef.bind(this);
}
/**
@ -101,7 +93,7 @@ class ChatInput extends Component<Props, State> {
componentDidMount() {
if (isMobileBrowser()) {
// Ensure textarea is not focused when opening chat on mobile browser.
this._textArea && this._textArea.blur();
this._textArea?.current && this._textArea.current.blur();
}
}
@ -134,7 +126,7 @@ class ChatInput extends Component<Props, State> {
onChange = { this._onMessageChange }
onKeyPress = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
ref = { this._setTextAreaRef }
ref = { this._textArea }
textarea = { true }
value = { this.state.message } />
<Button
@ -155,7 +147,7 @@ class ChatInput extends Component<Props, State> {
* @returns {void}
*/
_focus() {
this._textArea && this._textArea.focus();
this._textArea?.current && this._textArea.current.focus();
}
/**
@ -254,17 +246,6 @@ class ChatInput extends Component<Props, State> {
}
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
}
/**
* Sets the reference to the HTML TextArea.
*
* @param {HTMLAudioElement} textAreaElement - The HTML text area element.
* @private
* @returns {void}
*/
_setTextAreaRef(textAreaElement?: HTMLTextAreaElement) {
this._textArea = textAreaElement;
}
}
/**

View File

@ -26,6 +26,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<div
className = 'chatmessage-wrapper'
id = { this.props.message.messageId }
tabIndex = { -1 }>
<div
className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''} ${

View File

@ -44,15 +44,13 @@ class ChatMessageGroup extends Component<Props> {
return (
<div className = { `chat-message-group ${className}` }>
{
messages.map((message, i) => (
<ChatMessage
key = { i }
message = { message }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 } />
))
}
{ messages.map((message, i) => (
<ChatMessage
key = { i }
message = { message }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 } />
))}
</div>
);
}

View File

@ -1,129 +0,0 @@
// @flow
import React from 'react';
import { scrollIntoView } from 'seamless-scroll-polyfill';
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
/**
* Displays all received chat messages, grouped by sender.
*
* @augments AbstractMessageContainer
*/
export default class MessageContainer extends AbstractMessageContainer<Props> {
/**
* Whether or not chat has been scrolled to the bottom of the screen. Used
* to determine if chat should be scrolled automatically to the bottom when
* the {@code ChatInput} resizes.
*/
_isScrolledToBottom: boolean;
/**
* Reference to the HTML element at the end of the list of displayed chat
* messages. Used for scrolling to the end of the chat messages.
*/
_messagesListEndRef: Object;
/**
* A React ref to the HTML element containing all {@code ChatMessageGroup}
* instances.
*/
_messageListRef: Object;
/**
* Initializes a new {@code MessageContainer} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code MessageContainer} instance with.
*/
constructor(props: Props) {
super(props);
this._isScrolledToBottom = true;
this._messageListRef = React.createRef();
this._messagesListEndRef = React.createRef();
this._onChatScroll = this._onChatScroll.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const groupedMessages = this._getMessagesGroupedBySender();
const messages = groupedMessages.map((group, index) => {
const messageType = group[0] && group[0].messageType;
return (
<ChatMessageGroup
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
);
});
return (
<div
aria-labelledby = 'chat-header'
id = 'chatconversation'
onScroll = { this._onChatScroll }
ref = { this._messageListRef }
role = 'log'
tabIndex = { 0 }>
{ messages }
<div ref = { this._messagesListEndRef } />
</div>
);
}
/**
* Scrolls to the bottom again if the instance had previously been scrolled
* to the bottom. This method is used when a resize has occurred below the
* instance and bottom scroll needs to be maintained.
*
* @returns {void}
*/
maybeUpdateBottomScroll() {
if (this._isScrolledToBottom) {
this.scrollToBottom(false);
}
}
/**
* Automatically scrolls the displayed chat messages down to the latest.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling
* animation.
* @returns {void}
*/
scrollToBottom(withAnimation: boolean) {
scrollIntoView(this._messagesListEndRef.current, {
behavior: withAnimation ? 'smooth' : 'auto',
block: 'nearest'
});
}
_getMessagesGroupedBySender: () => Array<Array<Object>>;
_onChatScroll: () => void;
/**
* Callback invoked to listen to the current scroll location.
*
* @private
* @returns {void}
*/
_onChatScroll() {
const element = this._messageListRef.current;
this._isScrolledToBottom
= element.scrollHeight - element.scrollTop === element.clientHeight;
}
}

View File

@ -0,0 +1,313 @@
/* eslint-disable lines-around-comment */
import throttle from 'lodash/throttle';
import React, { RefObject } from 'react';
import { scrollIntoView } from 'seamless-scroll-polyfill';
// @ts-ignore
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { Props } from '../AbstractMessageContainer';
// @ts-ignore
import ChatMessageGroup from './ChatMessageGroup';
import NewMessagesButton from './NewMessagesButton';
interface State {
/**
* Whether or not message container has received new messages.
*/
hasNewMessages: boolean;
/**
* Whether or not scroll position is at the bottom of container.
*/
isScrolledToBottom: boolean;
/**
* The id of the last read message.
*/
lastReadMessageId: string;
}
/**
* Displays all received chat messages, grouped by sender.
*
* @augments AbstractMessageContainer
*/
export default class MessageContainer extends AbstractMessageContainer<Props, State> {
/**
* Component state used to decide when the hasNewMessages button to appear
* and where to scroll when click on hasNewMessages button.
*/
state: State = {
hasNewMessages: false,
isScrolledToBottom: true,
lastReadMessageId: ''
};
/**
* Reference to the HTML element at the end of the list of displayed chat
* messages. Used for scrolling to the end of the chat messages.
*/
_messagesListEndRef: RefObject<HTMLDivElement>;
/**
* A React ref to the HTML element containing all {@code ChatMessageGroup}
* instances.
*/
_messageListRef: RefObject<HTMLDivElement>;
/**
* Intersection observer used to detect intersections of messages with the bottom of the message container.
*/
_bottomListObserver: IntersectionObserver;
/**
* Initializes a new {@code MessageContainer} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code MessageContainer} instance with.
*/
constructor(props: Props) {
super(props);
this._messageListRef = React.createRef<HTMLDivElement>();
this._messagesListEndRef = React.createRef<HTMLDivElement>();
// Bind event handlers so they are only bound once for every instance.
this._handleIntersectBottomList = this._handleIntersectBottomList.bind(this);
this._findFirstUnreadMessage = this._findFirstUnreadMessage.bind(this);
this._isMessageVisible = this._isMessageVisible.bind(this);
this._onChatScroll = throttle(this._onChatScroll.bind(this), 300, { leading: true });
this._onGoToFirstUnreadMessage = this._onGoToFirstUnreadMessage.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const groupedMessages = this._getMessagesGroupedBySender();
const messages = groupedMessages.map((group, index) => {
const messageType = group[0] && group[0].messageType;
return (
<ChatMessageGroup
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
);
});
return (
<div id = 'chat-conversation-container'>
<div
aria-labelledby = 'chat-header'
id = 'chatconversation'
onScroll = { this._onChatScroll }
ref = { this._messageListRef }
role = 'log'
tabIndex = { 0 }>
{ messages }
{ !this.state.isScrolledToBottom && this.state.hasNewMessages
&& <NewMessagesButton
onGoToFirstUnreadMessage = { this._onGoToFirstUnreadMessage } /> }
<div
id = 'messagesListEnd'
ref = { this._messagesListEndRef } />
</div>
</div>
);
}
/**
* Implements {@code Component#componentDidMount}.
* When Component mount scroll message container to bottom.
* Create observer to react when scroll position is at bottom or leave the bottom.
*
* @inheritdoc
*/
componentDidMount() {
this.scrollToElement(false, null);
this._createBottomListObserver();
}
/**
* Implements {@code Component#componentDidUpdate}.
* If the user receive a new message scroll automatically to the bottom if scroll position was at the bottom.
* Otherwise update hasNewMessages from component state.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
const hasNewMessages = this.props.messages.length !== prevProps.messages.length;
if (hasNewMessages) {
if (this.state.isScrolledToBottom) {
this.scrollToElement(false, null);
} else {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ hasNewMessages: true });
}
}
}
/**
* Implements React's {@link Component#componentWillUnmount()}. Invoked
* immediately before this component is unmounted and destroyed.
*
* @inheritdoc
*/
componentWillUnmount() {
const target = document.querySelector('#messagesListEnd');
this._bottomListObserver.unobserve(target as Element);
}
/**
* Automatically scrolls the displayed chat messages to bottom or to a specific element if it is provided.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling.
* @param {TMLElement} element - Where to scroll.
* Animation.
* @returns {void}
*/
scrollToElement(withAnimation: boolean, element: Element | null) {
const scrollTo = element ? element : this._messagesListEndRef.current;
const block = element ? 'center' : 'nearest';
scrollIntoView(scrollTo as Element, {
behavior: withAnimation ? 'smooth' : 'auto',
block
});
}
/**
* Callback invoked to listen to current scroll position and update next unread message.
* The callback is invoked inside a throttle with 300 ms to decrease the number of function calls.
*
* @private
* @returns {void}
*/
_onChatScroll() {
const firstUnreadMessage = this._findFirstUnreadMessage();
if (firstUnreadMessage && firstUnreadMessage.id !== this.state.lastReadMessageId) {
this.setState({ lastReadMessageId: firstUnreadMessage?.id });
}
}
/**
* Find the first unread message.
* Update component state and scroll to element.
*
* @private
* @returns {void}
*/
_onGoToFirstUnreadMessage() {
const firstUnreadMessage = this._findFirstUnreadMessage();
this.setState({ lastReadMessageId: firstUnreadMessage?.id || null });
this.scrollToElement(true, firstUnreadMessage as Element);
}
/**
* Create observer to react when scroll position is at bottom or leave the bottom.
*
* @private
* @returns {void}
*/
_createBottomListObserver() {
const options = {
root: document.querySelector('#chatconversation'),
rootMargin: '35px',
threshold: 0.5
};
const target = document.querySelector('#messagesListEnd');
if (target) {
this._bottomListObserver = new IntersectionObserver(this._handleIntersectBottomList, options);
this._bottomListObserver.observe(target);
}
}
/** .
* _HandleIntersectBottomList.
* When entry is intersecting with bottom of container set last message as last read message.
* When entry is not intersecting update only isScrolledToBottom with false value.
*
* @param {Array} entries - List of entries.
* @private
* @returns {void}
*/
_handleIntersectBottomList(entries: IntersectionObserverEntry[]) {
entries.forEach((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting && this.props.messages.length) {
const lastMessageIndex = this.props.messages.length - 1;
const lastMessage = this.props.messages[lastMessageIndex];
const lastReadMessageId = lastMessage.messageId;
this.setState(
{
isScrolledToBottom: true,
hasNewMessages: false,
lastReadMessageId
});
}
if (!entry.isIntersecting) {
this.setState(
{
isScrolledToBottom: false
});
}
});
}
/**
* Find first unread message.
* MessageIsAfterLastSeenMessage filter elements which are not visible but are before the last read message.
*
* @private
* @returns {Element}
*/
_findFirstUnreadMessage() {
const messagesNodeList = document.querySelectorAll('.chatmessage-wrapper');
// @ts-ignore
const messagesToArray = [ ...messagesNodeList ];
const previousIndex = messagesToArray.findIndex((message: Element) =>
message.id === this.state.lastReadMessageId);
if (previousIndex !== -1) {
for (let i = previousIndex; i < messagesToArray.length; i++) {
if (!this._isMessageVisible(messagesToArray[i])) {
return messagesToArray[i];
}
}
}
}
/**
* Check if a message is visible in view.
*
* @param {Element} message -
*
* @returns {boolean}
*/
_isMessageVisible(message: Element): boolean {
const { bottom, height, top } = message.getBoundingClientRect();
if (this._messageListRef.current) {
const containerRect = this._messageListRef.current.getBoundingClientRect();
return top <= containerRect.top
? containerRect.top - top <= height : bottom - containerRect.bottom <= height;
}
return false;
}
}

View File

@ -0,0 +1,93 @@
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown } from '../../../base/icons/svg/index';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
export interface INewMessagesButtonProps extends WithTranslation {
/**
* Function to notify messageContainer when click on goToFirstUnreadMessage button.
*/
onGoToFirstUnreadMessage: () => void;
}
const useStyles = makeStyles((theme: any) => {
return {
container: {
position: 'absolute',
left: 'calc(50% - 72px)',
bottom: '15px'
},
newMessagesButton: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
height: '32px',
padding: '6px 8px',
border: 'none',
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.action02,
boxShadow: '0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25)',
'&:hover': {
backgroundColor: theme.palette.action02Hover
},
'&:active': {
backgroundColor: theme.palette.action02Active
}
},
arrowDownIconContainer: {
height: '20px',
width: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: '5px',
'& svg': {
fill: theme.palette.uiBackground
}
},
textContainer: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text04,
paddingLeft: '8px'
}
};
});
/** NewMessagesButton.
*
* @param {Function} onGoToFirstUnreadMessage - Function for lifting up onClick event.
* @returns {JSX.Element}
*/
function NewMessagesButton({ onGoToFirstUnreadMessage, t }: INewMessagesButtonProps): JSX.Element {
const styles = useStyles();
return (
<div
className = { styles.container }>
<button
aria-label = { t('chat.newMessages') }
className = { styles.newMessagesButton }
onClick = { onGoToFirstUnreadMessage }
type = 'button'>
<Icon
className = { styles.arrowDownIconContainer }
size = { 14 }
src = { IconArrowDown } />
<div className = { styles.textContainer }> { t('chat.newMessages') }</div>
</button>
</div>);
}
export default translate(NewMessagesButton);

View File

@ -27,7 +27,7 @@ const DEFAULT_STATE = {
isLobbyChatActive: false
};
interface IMessage {
export interface IMessage {
displayName: string;
error?: Object;
id: string;