diff --git a/css/_chat.scss b/css/_chat.scss index e7cc5da43b..eb61556dbd 100644 --- a/css/_chat.scss +++ b/css/_chat.scss @@ -130,7 +130,7 @@ border-radius:0; box-shadow: none; color: white; - font-size: 10pt; + font-size: 15px; line-height: 30px; padding: 5px; max-height:150px; @@ -162,25 +162,12 @@ } .display-name { - float: left; - padding-left: 5px; + font-size: 13px; font-weight: bold; white-space: nowrap; text-overflow: ellipsis; - width: 95%; overflow: hidden; } - - .timestamp { - float: right; - padding-right: 5px; - font-size: 11px; - } -} - -.usermessage { - padding-top: 20px; - padding-left: 5px; } .chatmessage { @@ -290,3 +277,41 @@ #usermsg::-webkit-scrollbar-track-piece { background: #3a3a3a; } + +.chat-message-group { + display: flex; + flex-direction: column; + + &.local { + align-items: flex-end; + + .display-name { + display: none; + } + + .chatmessage { + background-color: $chatLocalMessageBackgroundColor; + border-radius: 6px 0px 6px 6px; + } + } + + &.error { + .chatmessage { + border-radius: 0px; + color: red; + } + + .display-name { + display: none; + } + } + + .chatmessage { + background-color: $chatRemoteMessageBackgroundColor; + border-radius: 0px 6px 6px 6px; + display: inline-block; + margin-top: 3px; + color: white; + padding: 8px; + } +} diff --git a/react/features/chat/components/AbstractChatMessage.js b/react/features/chat/components/AbstractChatMessage.js index 00be6fa97a..4ef592c8e4 100644 --- a/react/features/chat/components/AbstractChatMessage.js +++ b/react/features/chat/components/AbstractChatMessage.js @@ -19,6 +19,12 @@ export type Props = { */ message: Object, + /** + * Whether or not the name of the participant which sent the message should + * be displayed. + */ + showDisplayName: boolean, + /** * Invoked to receive translated strings. */ @@ -28,7 +34,11 @@ export type Props = { /** * Abstract component to display a chat message. */ -export default class AbstractChatMessage extends PureComponent

{} +export default class AbstractChatMessage extends PureComponent

{ + static defaultProps = { + showDisplayName: true + }; +} /** * Maps part of the Redux state to the props of this component. diff --git a/react/features/chat/components/web/Chat.js b/react/features/chat/components/web/Chat.js index 294d59dc5c..6bcebac1ab 100644 --- a/react/features/chat/components/web/Chat.js +++ b/react/features/chat/components/web/Chat.js @@ -12,7 +12,7 @@ import AbstractChat, { type Props } from '../AbstractChat'; import ChatInput from './ChatInput'; -import ChatMessage from './ChatMessage'; +import ChatMessageGroup from './ChatMessageGroup'; import DisplayNameForm from './DisplayNameForm'; /** @@ -46,7 +46,6 @@ class Chat extends AbstractChat { this._messagesListEnd = null; // Bind event handlers so they are only bound once for every instance. - this._renderMessage = this._renderMessage.bind(this); this._renderPanelContent = this._renderPanelContent.bind(this); this._setMessageListEndRef = this._setMessageListEndRef.bind(this); } @@ -88,6 +87,37 @@ class Chat extends AbstractChat { ); } + /** + * Iterates over all the messages and creates nested arrays which hold + * consecutive messages sent be the same participant. + * + * @private + * @returns {Array>} + */ + _getMessagesGroupedBySender() { + const messagesCount = this.props._messages.length; + const groups = []; + let currentGrouping = []; + let currentGroupParticipantId; + + for (let i = 0; i < messagesCount; i++) { + const message = this.props._messages[i]; + + if (message.id === currentGroupParticipantId) { + currentGrouping.push(message); + } else { + groups.push(currentGrouping); + + currentGrouping = [ message ]; + currentGroupParticipantId = message.id; + } + } + + groups.push(currentGrouping); + + return groups; + } + /** * Returns a React Element for showing chat messages and a form to send new * chat messages. @@ -96,7 +126,25 @@ class Chat extends AbstractChat { * @returns {ReactElement} */ _renderChat() { - const messages = this.props._messages.map(this._renderMessage); + const groupedMessages = this._getMessagesGroupedBySender(); + + const messages = groupedMessages.map((group, index) => { + const messageType = group[0] && group[0].messageType; + let className = 'remote'; + + if (messageType === 'local') { + className = 'local'; + } else if (messageType === 'error') { + className = 'error'; + } + + return ( + + ); + }); messages.push(

{ ); } - _renderMessage: (Object) => void; - - /** - * Called by {@code _onSubmitMessage} to create the chat div. - * - * @param {string} message - The chat message to display. - * @param {string} id - The chat message ID to use as a unique key. - * @returns {Array} - */ - _renderMessage(message: Object, id: string) { - return ( - - ); - } - _renderPanelContent: (string) => React$Node | null; /** diff --git a/react/features/chat/components/web/ChatMessage.js b/react/features/chat/components/web/ChatMessage.js index d603fd61a7..e0ab91e627 100644 --- a/react/features/chat/components/web/ChatMessage.js +++ b/react/features/chat/components/web/ChatMessage.js @@ -23,24 +23,12 @@ class ChatMessage extends AbstractChatMessage { */ render() { const { message } = this.props; - let messageTypeClassname = ''; - let messageToDisplay = message.message; - - switch (message.messageType) { - case 'local': - messageTypeClassname = 'localuser'; - - break; - case 'error': - messageTypeClassname = 'error'; - messageToDisplay = this.props.t('chat.error', { + const messageToDisplay = message.messageType === 'error' + ? this.props.t('chat.error', { error: message.error, - originalText: messageToDisplay - }); - break; - default: - messageTypeClassname = 'remoteuser'; - } + originalText: message.message + }) + : message.message; // replace links and smileys // Strophe already escapes special symbols on sending, @@ -68,47 +56,16 @@ class ChatMessage extends AbstractChatMessage { }); return ( -
-
+
+ { this.props.showDisplayName &&
{ message.displayName } -
-
- { ChatMessage.formatTimestamp(message.timestamp) } -
+
}
{ processedMessage }
); } - - /** - * Returns a timestamp formatted for display. - * - * @param {number} timestamp - The timestamp for the chat message. - * @private - * @returns {string} - */ - static formatTimestamp(timestamp) { - const now = new Date(timestamp); - let hour = now.getHours(); - let minute = now.getMinutes(); - let second = now.getSeconds(); - - if (hour.toString().length === 1) { - hour = `0${hour}`; - } - - if (minute.toString().length === 1) { - minute = `0${minute}`; - } - - if (second.toString().length === 1) { - second = `0${second}`; - } - - return `${hour}:${minute}:${second}`; - } } export default translate(ChatMessage, { wait: false }); diff --git a/react/features/chat/components/web/ChatMessageGroup.js b/react/features/chat/components/web/ChatMessageGroup.js new file mode 100644 index 0000000000..fb317bcddd --- /dev/null +++ b/react/features/chat/components/web/ChatMessageGroup.js @@ -0,0 +1,68 @@ +// @flow + +import React, { Component } from 'react'; +import ChatMessage from './ChatMessage'; + +import { getLocalizedDateFormatter } from '../../../base/i18n'; + +type Props = { + + /** + * Additional CSS classes to apply to the root element. + */ + className: string, + + /** + * The messages to display as a group. + */ + messages: Array, +}; + +/** + * Displays a list of chat messages. Will show only the display name for the + * first chat message and the timestamp for the last chat message. + * + * @extends React.Component + */ +class ChatMessageGroup extends Component { + static defaultProps = { + className: '' + }; + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + const { className, messages } = this.props; + + const messagesLength = messages.length; + + if (!messagesLength) { + return null; + } + + const { timestamp } = messages[messagesLength - 1]; + + return ( +
+ { + messages.map((message, i) => ( +
+ +
)) + } +
+ { getLocalizedDateFormatter( + new Date(timestamp)).format('H:mm') } +
+
+ ); + } +} + +export default ChatMessageGroup;