feat: add chat color scheming

This commit is contained in:
Bettenbuk Zoltan 2019-10-15 16:08:23 +02:00 committed by Zoltan Bettenbuk
parent 8be02f9ca1
commit a35099f949
15 changed files with 302 additions and 142 deletions

View File

@ -153,6 +153,8 @@ class ColorSchemeRegistry {
const colorScheme = toState(stateful)['features/base/color-scheme'];
return {
...defaultScheme._defaultTheme,
...colorScheme._defaultTheme,
...defaultScheme[componentName],
...colorScheme[componentName]
}[colorDefinition];

View File

@ -6,18 +6,26 @@ import { ColorPalette, getRGBAFormat } from '../styles';
* The default color scheme of the application.
*/
export default {
'BottomSheet': {
'_defaultTheme': {
// Generic app theme colors that are used accross the entire app.
// All scheme definitions below inherit these values.
background: 'rgb(255, 255, 255)',
icon: '#1c2025',
label: '#1c2025'
icon: 'rgb(28, 32, 37)',
text: 'rgb(28, 32, 37)'
},
'Chat': {
displayName: 'rgb(94, 109, 121)',
localMsgBackground: 'rgb(215, 230, 249)',
privateMsgBackground: 'rgb(250, 219, 219)',
privateMsgNotice: 'rgb(186, 39, 58)',
remoteMsgBackground: 'rgb(241, 242, 246)',
replyBorder: 'rgb(219, 197, 200)',
replyIcon: 'rgb(94, 109, 121)'
},
'Dialog': {
background: 'rgb(255, 255, 255)',
border: 'rgba(0, 3, 6, 0.6)',
buttonBackground: ColorPalette.blue,
buttonLabel: ColorPalette.white,
icon: '#1c2025',
text: '#1c2025'
buttonLabel: ColorPalette.white
},
'Header': {
background: ColorPalette.blue,
@ -30,8 +38,7 @@ export default {
background: 'rgb(42, 58, 75)'
},
'LoadConfigOverlay': {
background: 'rgb(249, 249, 249)',
text: 'rgb(28, 32, 37)'
background: 'rgb(249, 249, 249)'
},
'Thumbnail': {
activeParticipantHighlight: 'rgb(81, 214, 170)',

View File

@ -147,7 +147,7 @@ ColorSchemeRegistry.register('BottomSheet', {
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
color: schemeColor('label'),
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: 32,

View File

@ -4,6 +4,8 @@ import { PureComponent } from 'react';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
/**
* Formatter string to display the message timestamp.
*/
@ -65,7 +67,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
_getMessageText() {
const { message } = this.props;
return message.messageType === 'error'
return message.messageType === MESSAGE_TYPE_ERROR
? this.props.t('chat.error', {
error: message.message
})
@ -81,7 +83,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : t('chat.you')
});
}
}

View File

@ -6,7 +6,7 @@ import { getParticipantDisplayName } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
type Props = {
export type Props = {
/**
* Function used to translate i18n labels.
@ -27,7 +27,7 @@ type Props = {
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient extends PureComponent<Props> {
export default class AbstractMessageRecipient<P: Props> extends PureComponent<P> {
}

View File

@ -3,15 +3,16 @@
import React from 'react';
import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractChat, {
_mapDispatchToProps,
_mapStateToProps,
type Props
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractChat';
import ChatInputBar from './ChatInputBar';
@ -19,6 +20,14 @@ import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Implements a React native component that renders the chat window (modal) of
* the mobile client.
@ -41,6 +50,8 @@ class Chat extends AbstractChat<Props> {
* @inheritdoc
*/
render() {
const { _styles } = this.props;
return (
<SlidingView
onHide = { this._onClose }
@ -52,7 +63,7 @@ class Chat extends AbstractChat<Props> {
<HeaderWithNavigation
headerLabelKey = 'chat.title'
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<SafeAreaView style = { _styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<MessageRecipient />
<ChatInputBar onSend = { this.props._onSendMessage } />
@ -80,4 +91,17 @@ class Chat extends AbstractChat<Props> {
}
}
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@ -4,16 +4,28 @@ import React from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import AbstractChatMessage, { type Props as AbstractProps } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Renders a single chat message.
*/
@ -24,55 +36,58 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @inheritdoc
*/
render() {
const { message } = this.props;
const localMessage = message.messageType === 'local';
const { _styles, message } = this.props;
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
const { privateMessage } = message;
// Style arrays that need to be updated in various scenarios, such as
// error messages or others.
const detailsWrapperStyle = [
styles.detailsWrapper
];
const textWrapperStyle = [
styles.textWrapper
const messageBubbleStyle = [
styles.messageBubble
];
if (localMessage) {
// This is a message sent by the local participant.
// The wrapper needs to be aligned to the right.
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper);
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.ownTextWrapper);
} else if (message.messageType === 'error') {
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.systemTextWrapper);
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.localMessageBubble);
} else if (message.messageType === MESSAGE_TYPE_ERROR) {
// This is a system message.
// The bubble needs some additional styling
messageBubbleStyle.push(styles.systemMessageBubble);
} else {
// This is a remote message sent by a remote participant.
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.remoteMessageBubble);
}
if (privateMessage) {
messageBubbleStyle.push(_styles.privateMessageBubble);
}
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { styles.replyWrapper }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<View style = { messageBubbleStyle }>
<View style = { styles.textWrapper } >
{ this._renderDisplayName() }
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{
message.privateMessage
&& this._renderPrivateNotice()
}
{ this._renderPrivateNotice() }
</View>
{ message.privateMessage && !localMessage
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } /> }
{ this._renderPrivateReplyButton() }
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
{ this._renderTimestamp() }
</View>
</View>
);
@ -104,37 +119,77 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
/**
* Renders the display name of the sender.
* Renders the display name of the sender if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderDisplayName() {
const { _styles, message, showDisplayName } = this.props;
if (!showDisplayName) {
return null;
}
return (
<Text style = { styles.displayName }>
{ this.props.message.displayName }
<Text style = { _styles.displayName }>
{ message.displayName }
</Text>
);
}
/**
* Renders the message privacy notice.
* Renders the message privacy notice, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateNotice() {
const { _styles, message } = this.props;
if (!message.privateMessage) {
return null;
}
return (
<Text style = { styles.privateNotice }>
<Text style = { _styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
}
/**
* Renders the time at which the message was sent.
* Renders the private reply button, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateReplyButton() {
const { _styles, message } = this.props;
const { messageType, privateMessage } = message;
if (!privateMessage || messageType === MESSAGE_TYPE_LOCAL) {
return null;
}
return (
<View style = { _styles.replyContainer }>
<PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { _styles.replyStyles } />
</View>
);
}
/**
* Renders the time at which the message was sent, if necessary.
*
* @returns {React$Element<*> | null}
*/
_renderTimestamp() {
if (!this.props.showTimestamp) {
return null;
}
return (
<Text style = { styles.timeText }>
{ this._getFormattedTimestamp() }
@ -143,4 +198,16 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
export default translate(ChatMessage);
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@ -3,6 +3,8 @@
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import ChatMessage from './ChatMessage';
import styles from './styles';
@ -73,11 +75,11 @@ export default class ChatMessageGroup extends Component<Props> {
<ChatMessage
message = { message }
showAvatar = {
this.props.messages[0].messageType !== 'local'
this.props.messages[0].messageType !== MESSAGE_TYPE_LOCAL
&& index === this.props.messages.length - 1
}
showDisplayName = {
this.props.messages[0].messageType === 'remote'
this.props.messages[0].messageType === MESSAGE_TYPE_REMOTE
&& index === this.props.messages.length - 1
}
showTimestamp = { index === 0 } />

View File

@ -3,28 +3,37 @@
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractMessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
const { _privateMessageRecipient, _styles } = this.props;
if (!_privateMessageRecipient) {
return null;
@ -33,8 +42,8 @@ class MessageRecipient extends AbstractMessageRecipient {
const { t } = this.props;
return (
<View style = { styles.messageRecipientContainer }>
<Text style = { styles.messageRecipientText }>
<View style = { _styles.messageRecipientContainer }>
<Text style = { _styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
@ -42,11 +51,24 @@ class MessageRecipient extends AbstractMessageRecipient {
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { styles.messageRecipientCancelIcon } />
style = { _styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
}
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@ -1,7 +1,10 @@
// @flow
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { BoxModel, ColorPalette } from '../../../base/styles';
const BUBBLE_RADIUS = 8;
/**
* The styles of the feature chat.
*
@ -20,14 +23,6 @@ export default {
width: 32
},
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: ColorPalette.white,
flex: 1
},
chatContainer: {
alignItems: 'stretch',
flex: 1,
@ -47,14 +42,6 @@ export default {
flexDirection: 'column'
},
/**
* The text node for the display name.
*/
displayName: {
color: 'rgb(118, 136, 152)',
fontSize: 13
},
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
@ -76,35 +63,16 @@ export default {
height: 48
},
messageBubble: {
alignItems: 'center',
borderRadius: BUBBLE_RADIUS,
flexDirection: 'row'
},
messageContainer: {
flex: 1
},
messageRecipientCancelIcon: {
color: ColorPalette.white,
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.warning,
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: ColorPalette.white,
flex: 1
},
/**
* The message text itself.
*/
messageText: {
color: 'rgb(28, 32, 37)',
fontSize: 15
},
/**
* Wrapper View for the entire block.
*/
@ -123,34 +91,11 @@ export default {
alignItems: 'flex-end'
},
/**
* Style modifier for the {@code textWrapper} for own messages.
*/
ownTextWrapper: {
backgroundColor: 'rgb(210, 231, 249)',
borderTopLeftRadius: 8,
borderTopRightRadius: 0
},
replyWrapper: {
alignItems: 'center',
flexDirection: 'row'
},
replyStyles: {
iconStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 22,
margin: BoxModel.margin / 2
}
},
privateNotice: {
color: ColorPalette.warning,
fontSize: 13,
fontStyle: 'italic'
},
sendButtonIcon: {
color: ColorPalette.darkGrey,
fontSize: 22
@ -159,7 +104,7 @@ export default {
/**
* Style modifier for system (error) messages.
*/
systemTextWrapper: {
systemMessageBubble: {
backgroundColor: 'rgb(247, 215, 215)'
},
@ -168,9 +113,6 @@ export default {
*/
textWrapper: {
alignItems: 'flex-start',
backgroundColor: 'rgb(240, 243, 247)',
borderRadius: 8,
borderTopLeftRadius: 0,
flexDirection: 'column',
padding: 9
},
@ -183,3 +125,73 @@ export default {
fontSize: 13
}
};
ColorSchemeRegistry.register('Chat', {
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: schemeColor('background'),
flex: 1
},
/**
* The text node for the display name.
*/
displayName: {
color: schemeColor('displayName'),
fontSize: 13
},
localMessageBubble: {
backgroundColor: schemeColor('localMsgBackground'),
borderTopRightRadius: 0
},
messageRecipientCancelIcon: {
color: schemeColor('icon'),
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: schemeColor('privateMsgBackground'),
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: schemeColor('text'),
flex: 1
},
privateNotice: {
color: schemeColor('privateMsgNotice'),
fontSize: 11,
marginTop: 6
},
privateMessageBubble: {
backgroundColor: schemeColor('privateMsgBackground')
},
remoteMessageBubble: {
backgroundColor: schemeColor('remoteMsgBackground'),
borderTopLeftRadius: 0
},
replyContainer: {
alignSelf: 'stretch',
borderLeftColor: schemeColor('replyBorder'),
borderLeftWidth: 1,
justifyContent: 'center'
},
replyStyles: {
iconStyle: {
color: schemeColor('replyIcon'),
fontSize: 22,
padding: 8
}
}
});

View File

@ -7,6 +7,8 @@ import { toArray } from 'react-emoji-render';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
@ -47,7 +49,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
</div>
{ message.privateMessage && this._renderPrivateNotice() }
</div>
{ message.privateMessage && message.messageType !== 'local'
{ message.privateMessage && message.messageType !== MESSAGE_TYPE_LOCAL
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }

View File

@ -2,6 +2,8 @@
import React from 'react';
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
@ -61,7 +63,7 @@ export default class MessageContainer extends AbstractMessageContainer {
return (
<ChatMessageGroup
className = { messageType || 'remote' }
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
);

View File

@ -8,13 +8,14 @@ import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps,
type Props
} from '../AbstractMessageRecipient';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*

View File

@ -1,3 +1,5 @@
// @flow
/**
* The audio ID of the audio element for which the {@link playAudio} action is
* triggered when new chat message is received.
@ -5,3 +7,18 @@
* @type {string}
*/
export const INCOMING_MSG_SOUND_ID = 'INCOMING_MSG_SOUND';
/**
* The {@code messageType} of error (system) messages.
*/
export const MESSAGE_TYPE_ERROR = 'error';
/**
* The {@code messageType} of local messages.
*/
export const MESSAGE_TYPE_LOCAL = 'local';
/**
* The {@code messageType} of remote messages.
*/
export const MESSAGE_TYPE_REMOTE = 'remote';

View File

@ -22,7 +22,7 @@ import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID } from './constants';
import { INCOMING_MSG_SOUND_ID, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
@ -194,7 +194,7 @@ function _addChatMsgListener(conference, store) {
function _handleChatError({ dispatch }, error) {
dispatch(addMessage({
hasRead: true,
messageType: 'error',
messageType: MESSAGE_TYPE_ERROR,
message: error,
privateMessage: false,
timestamp: Date.now()
@ -231,7 +231,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, pri
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
@ -285,7 +285,7 @@ function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message
displayName,
hasRead: true,
id: localParticipant.id,
messageType: 'local',
messageType: MESSAGE_TYPE_LOCAL,
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
@ -323,7 +323,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const lastMessage = navigator.product === 'ReactNative'
? messages[0] : messages[messages.length - 1];
if (lastMessage.messageType === 'local') {
if (lastMessage.messageType === MESSAGE_TYPE_LOCAL) {
// The sender is probably aware of any private messages as already sent
// a message since then. Doesn't make sense to display the notice now.
return undefined;
@ -339,7 +339,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const now = Date.now();
const recentPrivateMessages = messages.filter(
message =>
message.messageType !== 'local'
message.messageType !== MESSAGE_TYPE_LOCAL
&& message.privateMessage
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
const recentPrivateMessage = navigator.product === 'ReactNative'