feat(chat): on web, group messages by sender
This commit is contained in:
parent
f5ac18da18
commit
fb5a45f714
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<P: Props> extends PureComponent<P> {}
|
||||
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
|
||||
static defaultProps = {
|
||||
showDisplayName: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
|
|
|
@ -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<Props> {
|
|||
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<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all the messages and creates nested arrays which hold
|
||||
* consecutive messages sent be the same participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Array<Object>>}
|
||||
*/
|
||||
_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<Props> {
|
|||
* @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 (
|
||||
<ChatMessageGroup
|
||||
className = { className }
|
||||
key = { index }
|
||||
messages = { group } />
|
||||
);
|
||||
});
|
||||
|
||||
messages.push(<div
|
||||
key = 'end-marker'
|
||||
|
@ -129,23 +177,6 @@ class Chat extends AbstractChat<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
_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<ReactElement>}
|
||||
*/
|
||||
_renderMessage(message: Object, id: string) {
|
||||
return (
|
||||
<ChatMessage
|
||||
key = { id }
|
||||
message = { message } />
|
||||
);
|
||||
}
|
||||
|
||||
_renderPanelContent: (string) => React$Node | null;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,24 +23,12 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
*/
|
||||
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<Props> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className = { `chatmessage ${messageTypeClassname}` }>
|
||||
<div className = 'display-name'>
|
||||
<div className = 'chatmessage'>
|
||||
{ this.props.showDisplayName && <div className = 'display-name'>
|
||||
{ message.displayName }
|
||||
</div>
|
||||
<div className = { 'timestamp' }>
|
||||
{ ChatMessage.formatTimestamp(message.timestamp) }
|
||||
</div>
|
||||
</div> }
|
||||
<div className = 'usermessage'>
|
||||
{ processedMessage }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
|
|
|
@ -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<Object>,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<Props> {
|
||||
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 (
|
||||
<div className = { `chat-message-group ${className}` }>
|
||||
{
|
||||
messages.map((message, i) => (
|
||||
<div key = { i }>
|
||||
<ChatMessage
|
||||
key = { i }
|
||||
message = { message }
|
||||
showDisplayName = { i === 0 } />
|
||||
</div>))
|
||||
}
|
||||
<div className = 'chat-message-group-footer'>
|
||||
{ getLocalizedDateFormatter(
|
||||
new Date(timestamp)).format('H:mm') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatMessageGroup;
|
Loading…
Reference in New Issue