feat(chat) Redesign chat

Move some styles from SCSS to JSS
Convert some files to TS
Implement redesign
This commit is contained in:
robertpin 2023-01-17 12:36:01 +02:00 committed by Saúl Ibarra Corretgé
parent e3166e6faa
commit 8e1d96cc48
18 changed files with 507 additions and 508 deletions

View File

@ -32,7 +32,7 @@
#chat-conversation-container {
// extract message input height
height: calc(100% - 68px);
height: calc(100% - 64px);
overflow: hidden;
position: relative;
}
@ -76,32 +76,6 @@
}
}
#chat-recipient {
align-items: center;
background-color: $chatPrivateMessageBackgroundColor;
display: flex;
flex-direction: row;
font-weight: 100;
padding: 10px;
span {
color: white;
display: flex;
flex: 1;
}
div {
svg {
cursor: pointer;
fill: white;
}
}
&.lobby-chat-recipient {
background-color: $chatLobbyMessageBackgroundColor;
}
}
.chat-header {
height: 70px;
@ -124,13 +98,12 @@
}
.chat-input-container {
padding: 0 16px 16px;
padding: 0 16px 24px;
}
#chat-input {
display: flex;
align-items: flex-end;
padding: 4px;
position: relative;
}
@ -263,15 +236,6 @@
-webkit-user-select: text;
user-select: text;
}
.display-name {
font-size: 12px;
font-weight: 600;
margin-bottom: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.sr-only {
@ -288,24 +252,11 @@
}
.chatmessage {
background-color: $chatRemoteMessageBackgroundColor;
border-radius: 0px 6px 6px 6px;
box-sizing: border-box;
color: white;
margin-top: 3px;
max-width: 100%;
position: relative;
&.localuser {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
}
.usermessage {
white-space: pre-wrap;
font-size: 14px;
}
&.error {
border-radius: 0px;
@ -320,22 +271,12 @@
}
}
.privatemessagenotice {
font-size: 11px;
font-weight: 100;
}
.messagecontent {
margin: 8px;
max-width: 100%;
overflow: hidden;
}
}
.timestamp {
color: #757575;
}
#smileys {
font-size: 20pt;
margin: auto;
@ -409,24 +350,9 @@
}
.chat-message-group {
display: flex;
flex-direction: column;
&.local {
align-items: flex-end;
.chatmessage {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
&.lobbymessage {
background-color: $chatLobbyMessageBackgroundColor;
}
}
.display-name {
display: none;
}
@ -437,58 +363,10 @@
}
&.error {
.chatmessage {
background-color: $defaultWarningColor;
border-radius: 0px;
font-weight: 100;
}
.display-name {
display: none;
}
}
.chatmessage-wrapper {
max-width: 100%;
.replywrapper {
display: flex;
flex-direction: row;
align-items: center;
.messageactions {
align-self: stretch;
border-left: 1px solid $chatActionsSeparatorColor;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px;
&.lobbychatmessageactions {
border-left-color: $chatLobbyActionsSeparatorColor;
}
.toolbox-icon {
cursor: pointer;
}
}
}
}
.chatmessage {
background-color: $chatRemoteMessageBackgroundColor;
border-radius: 0px 6px 6px 6px;
display: inline-block;
margin-top: 3px;
color: white;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
&.lobbymessage {
background-color: $chatLobbyMessageBackgroundColor;
}
}
}
.chat-dialog {

View File

@ -79,7 +79,6 @@ $modalTextColor: #333;
$chatActionsSeparatorColor: rgb(173, 105, 112);
$chatBackgroundColor: #131519;
$chatInputSeparatorColor: #A4B8D1;
$chatLobbyMessageBackgroundColor: #6A50D3;
$chatLobbyActionsSeparatorColor: #6A50D3;
$chatLocalMessageBackgroundColor: #484A4F;
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);

19
package-lock.json generated
View File

@ -147,6 +147,7 @@
"@types/js-md5": "0.4.3",
"@types/lodash": "4.14.182",
"@types/react": "17.0.14",
"@types/react-linkify": "1.0.1",
"@types/react-native": "0.68.9",
"@types/react-redux": "7.1.24",
"@types/react-window": "1.8.5",
@ -6539,6 +6540,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-linkify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.1.tgz",
"integrity": "sha512-qPxYwjB41ezoKdLXs0MrQ1FnhF3apyyxf3J7WVQQCBu/GyZQAW7Y3TY4317jdh0450QJ4fLqj0rnhIJvFZOamQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-native": {
"version": "0.68.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz",
@ -25265,6 +25275,15 @@
"@types/react": "*"
}
},
"@types/react-linkify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.1.tgz",
"integrity": "sha512-qPxYwjB41ezoKdLXs0MrQ1FnhF3apyyxf3J7WVQQCBu/GyZQAW7Y3TY4317jdh0450QJ4fLqj0rnhIJvFZOamQ==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native": {
"version": "0.68.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz",

View File

@ -152,6 +152,7 @@
"@types/js-md5": "0.4.3",
"@types/lodash": "4.14.182",
"@types/react": "17.0.14",
"@types/react-linkify": "1.0.1",
"@types/react-native": "0.68.9",
"@types/react-redux": "7.1.24",
"@types/react-window": "1.8.5",

View File

@ -1,21 +1,19 @@
// @flow
import punycode from 'punycode';
import React, { Component } from 'react';
import React, { Component, ReactNode } from 'react';
import ReactLinkify from 'react-linkify';
type Props = {
interface IProps {
/**
* The children of the component.
*/
children: React$Node
};
children: ReactNode;
}
/**
* Implements a react wrapper for the react-linkify component.
*/
export default class Linkify extends Component<Props> {
export default class Linkify extends Component<IProps> {
/**
* Implements {@Component#render}.
*

View File

@ -1,11 +1,9 @@
// @flow
import React, { Component } from 'react';
import React, { Component, ReactNode } from 'react';
import { toArray } from 'react-emoji-render';
import GifMessage from '../../../../chat/components/web/GifMessage';
import { GIF_PREFIX } from '../../../../gifs/constants';
import { isGifMessage } from '../../../../gifs/functions';
import { isGifMessage } from '../../../../gifs/functions.web';
import Linkify from './Linkify';
@ -14,7 +12,7 @@ type Props = {
/**
* The body of the message.
*/
text: string
text: string;
};
/**
@ -41,7 +39,7 @@ class Message extends Component<Props> {
*/
_processMessage() {
const { text } = this.props;
const message = [];
const message: (string | ReactNode)[] = [];
// Tokenize the text in order to avoid emoji substitution for URLs
const tokens = text ? text.split(' ') : [];
@ -81,8 +79,6 @@ class Message extends Component<Props> {
return message;
}
_processMessage: () => Array<string | React$Element<*>>;
/**
* Implements React's {@link Component#render()}.
*

View File

@ -120,7 +120,7 @@ export function sendMessage(message: string, ignorePrivacy = false) {
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }}
*/
export function setPrivateMessageRecipient(participant: Object) {
export function setPrivateMessageRecipient(participant?: Object) {
return {
participant,
type: SET_PRIVATE_MESSAGE_RECIPIENT

View File

@ -1,9 +1,9 @@
// @flow
import { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { getLocalizedDateFormatter } from '../../base/i18n/dateUtil';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
import { IMessage } from '../reducer';
/**
* Formatter string to display the message timestamp.
@ -13,46 +13,41 @@ const TIMESTAMP_FORMAT = 'H:mm';
/**
* The type of the React {@code Component} props of {@code AbstractChatMessage}.
*/
export type Props = {
export interface IProps extends WithTranslation {
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean;
/**
* The representation of a chat message.
*/
message: Object,
message: IMessage;
/**
* Whether or not the avatar image of the participant which sent the message
* should be displayed.
*/
showAvatar: boolean,
showAvatar?: boolean;
/**
* Whether or not the name of the participant which sent the message should
* be displayed.
*/
showDisplayName: boolean,
showDisplayName: boolean;
/**
* Whether or not the time at which the message was sent should be
* displayed.
*/
showTimestamp: boolean,
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean,
/**
* Invoked to receive translated strings.
*/
t: Function
};
showTimestamp: boolean;
}
/**
* Abstract component to display a chat message.
*/
export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
export default class AbstractChatMessage<P extends IProps> extends PureComponent<P> {
/**
* Returns the timestamp to display for the message.
*

View File

@ -1,53 +1,50 @@
// @flow
import { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
import { IReduxState } from '../../app/types';
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
import { setLobbyChatActiveState } from '../actions.any';
import { setPrivateMessageRecipient } from '../actions.web';
export type Props = {
export interface IProps extends WithTranslation {
/**
* Function used to translate i18n labels.
*/
t: Function,
* Is lobby messaging active.
*/
_isLobbyChatActive: boolean;
/**
* Function to remove the recipent setting of the chat window.
*/
_onRemovePrivateMessageRecipient: Function,
* The name of the lobby message recipient, if any.
*/
_lobbyMessageRecipient?: string;
/**
/**
* Function to make the lobby message recipient inactive.
*/
_onHideLobbyChatRecipient: Function,
_onHideLobbyChatRecipient: () => void;
/**
* Function to remove the recipient setting of the chat window.
*/
_onRemovePrivateMessageRecipient: () => void;
/**
* The name of the message recipient, if any.
*/
_privateMessageRecipient: ?string,
_privateMessageRecipient?: string;
/**
* Is lobby messaging active.
*/
_isLobbyChatActive: boolean,
/**
* The name of the lobby message recipient, if any.
*/
_lobbyMessageRecipient: ?string,
/**
/**
* Shows widget if it is necessary.
*/
_visible: boolean;
};
_visible: boolean;
classes?: any;
}
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient<P: Props> extends PureComponent<P> {
export default class AbstractMessageRecipient<P extends IProps> extends PureComponent<P> {
}
@ -55,9 +52,9 @@ export default class AbstractMessageRecipient<P: Props> extends PureComponent<P>
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
* @returns {IProps}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
export function _mapDispatchToProps(dispatch: Function) {
return {
_onRemovePrivateMessageRecipient: () => {
dispatch(setPrivateMessageRecipient());
@ -72,9 +69,9 @@ export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
* @returns {IProps}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
export function _mapStateToProps(state: IReduxState) {
const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
return {

View File

@ -1,131 +0,0 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import Message from '../../../base/react/components/web/Message';
import { connect } from '../../../base/redux';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import PrivateMessageButton from './PrivateMessageButton';
/**
* Renders a single chat message.
*/
class ChatMessage extends AbstractChatMessage<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { message, t, knocking } = this.props;
return (
<div
className = 'chatmessage-wrapper'
id = { this.props.message.messageId }
tabIndex = { -1 }>
<div
className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''} ${
message.lobbyChat && !knocking ? 'lobbymessage' : ''}` }>
<div className = 'replywrapper'>
<div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
<span className = 'sr-only'>
{ this.props.message.displayName === this.props.message.recipient
? t('chat.messageAccessibleTitleMe')
: t('chat.messageAccessibleTitle',
{ user: this.props.message.displayName }) }
</span>
<Message text = { this._getMessageText() } />
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& this._renderPrivateNotice() }
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div
className = { `messageactions ${
message.lobbyChat ? 'lobbychatmessageactions' : ''}` }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id }
reply = { true }
showLabel = { false } />
</div>
) }
</div>
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
);
}
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the display name of the sender.
*
* @returns {React$Element<*>}
*/
_renderDisplayName() {
return (
<div
aria-hidden = { true }
className = 'display-name'>
{ this.props.message.displayName }
</div>
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<div className = 'privatemessagenotice'>
{ this._getPrivateNoticeMessage() }
</div>
);
}
/**
* Renders the time at which the message was sent.
*
* @returns {React$Element<*>}
*/
_renderTimestamp() {
return (
<div className = 'timestamp'>
{ this._getFormattedTimestamp() }
</div>
);
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
const { knocking } = state['features/lobby'];
return {
knocking
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@ -0,0 +1,223 @@
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import clsx from 'clsx';
import React from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Message from '../../../base/react/components/web/Message';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, { IProps as AbstractProps } from '../AbstractChatMessage';
import PrivateMessageButton from './PrivateMessageButton';
interface IProps extends AbstractProps {
classes: any;
type: string;
}
const styles = (theme: Theme) => {
return {
chatMessageWrapper: {
maxWidth: '100%'
},
chatMessage: {
display: 'inline-flex',
padding: '12px',
backgroundColor: theme.palette.ui02,
borderRadius: '4px 12px 12px 12px',
boxSizing: 'border-box' as const,
maxWidth: '100%',
marginTop: '4px',
'&.privatemessage': {
backgroundColor: theme.palette.support05
},
'&.local': {
backgroundColor: theme.palette.ui04,
borderRadius: '12px 4px 12px 12px',
'&.privatemessage': {
backgroundColor: theme.palette.support05
}
},
'&.error': {
backgroundColor: 'rgb(215, 121, 118)',
borderRadius: 0,
fontWeight: 100
},
'&.lobbymessage': {
backgroundColor: theme.palette.support05
}
},
replyWrapper: {
display: 'flex',
flexDirection: 'row' as const,
alignItems: 'center'
},
messageContent: {
maxWidth: '100%',
overflow: 'hidden',
flex: 1
},
replyButtonContainer: {
display: 'flex',
alignItems: 'flex-start',
height: '100%'
},
replyButton: {
padding: '2px'
},
displayName: {
...withPixelLineHeight(theme.typography.labelBold),
color: theme.palette.text02,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
marginBottom: theme.spacing(1)
},
userMessage: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
whiteSpace: 'pre-wrap'
},
privateMessageNotice: {
...withPixelLineHeight(theme.typography.labelRegular),
color: theme.palette.text02,
marginTop: theme.spacing(1)
},
timestamp: {
...withPixelLineHeight(theme.typography.labelRegular),
color: theme.palette.text03,
marginTop: theme.spacing(1)
}
};
};
/**
* Renders a single chat message.
*/
class ChatMessage extends AbstractChatMessage<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { message, t, knocking, classes, type } = this.props;
return (
<div
className = { classes.chatMessageWrapper }
id = { this.props.message.messageId }
tabIndex = { -1 }>
<div
className = { clsx('chatmessage', classes.chatMessage, type,
message.privateMessage && 'privatemessage',
message.lobbyChat && !knocking && 'lobbymessage') }>
<div className = { classes.replyWrapper }>
<div className = { clsx('messagecontent', classes.messageContent) }>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = { clsx('usermessage', classes.userMessage) }>
<span className = 'sr-only'>
{ this.props.message.displayName === this.props.message.recipient
? t('chat.messageAccessibleTitleMe')
: t('chat.messageAccessibleTitle',
{ user: this.props.message.displayName }) }
</span>
<Message text = { this._getMessageText() } />
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& this._renderPrivateNotice() }
</div>
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div
className = { classes.replyButtonContainer }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id } />
</div>
) }
</div>
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
);
}
/**
* Renders the display name of the sender.
*
* @returns {React$Element<*>}
*/
_renderDisplayName() {
return (
<div
aria-hidden = { true }
className = { clsx('display-name', this.props.classes.displayName) }>
{ this.props.message.displayName }
</div>
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<div className = { this.props.classes.privateMessageNotice }>
{ this._getPrivateNoticeMessage() }
</div>
);
}
/**
* Renders the time at which the message was sent.
*
* @returns {React$Element<*>}
*/
_renderTimestamp() {
return (
<div className = { clsx('timestamp', this.props.classes.timestamp) }>
{ this._getFormattedTimestamp() }
</div>
);
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { knocking } = state['features/lobby'];
return {
knocking
};
}
export default translate(connect(_mapStateToProps)(withStyles(styles)(ChatMessage)));

View File

@ -1,59 +0,0 @@
// @flow
import React, { Component } from 'react';
import ChatMessage from './ChatMessage';
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.
*
* @augments 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;
}
return (
<div className = { `chat-message-group ${className}` }>
{ messages.map((message, i) => (
<ChatMessage
key = { i }
message = { message }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 } />
))}
</div>
);
}
}
export default ChatMessageGroup;

View File

@ -0,0 +1,81 @@
import clsx from 'clsx';
import React from 'react';
import { makeStyles } from 'tss-react/mui';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import Avatar from '../../../base/avatar/components/Avatar';
import { IMessage } from '../../reducer';
import ChatMessage from './ChatMessage';
interface IProps {
/**
* Additional CSS classes to apply to the root element.
*/
className: string;
/**
* The messages to display as a group.
*/
messages: Array<IMessage>;
}
const useStyles = makeStyles()(theme => {
return {
messageGroup: {
display: 'flex',
flexDirection: 'column'
},
groupContainer: {
display: 'flex',
'&.local': {
justifyContent: 'flex-end',
'& .avatar': {
display: 'none'
}
}
},
avatar: {
margin: `${theme.spacing(1)} ${theme.spacing(2)} ${theme.spacing(3)} 0`,
position: 'sticky',
top: 0
}
};
});
const ChatMessageGroup = ({ className = '', messages }: IProps) => {
const { classes } = useStyles();
const messagesLength = messages.length;
if (!messagesLength) {
return null;
}
return (
<div className = { clsx(classes.groupContainer, className) }>
<Avatar
className = { clsx(classes.avatar, 'avatar') }
participantId = { messages[0].id }
size = { 32 } />
<div className = { `${classes.messageGroup} chat-message-group ${className}` }>
{messages.map((message, i) => (
<ChatMessage
key = { i }
message = { message }
showDisplayName = { i === 0 }
showTimestamp = { i === messages.length - 1 }
type = { className } />
))}
</div>
</div>
);
};
export default ChatMessageGroup;

View File

@ -5,7 +5,6 @@ import { scrollIntoView } from 'seamless-scroll-polyfill';
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { IProps } from '../AbstractMessageContainer';
// @ts-ignore
import ChatMessageGroup from './ChatMessageGroup';
import NewMessagesButton from './NewMessagesButton';

View File

@ -1,35 +1,61 @@
// @flow
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconCloseCircle } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { translate } from '../../../base/i18n/functions';
import { IconCloseLarge } from '../../../base/icons/svg';
import { connect } from '../../../base/redux/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import AbstractMessageRecipient, {
type Props,
IProps,
_mapDispatchToProps,
_mapStateToProps
} from '../AbstractMessageRecipient';
const styles = (theme: Theme) => {
return {
container: {
margin: '0 16px 8px',
padding: '6px',
paddingLeft: '16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: theme.palette.support05,
borderRadius: theme.shape.borderRadius,
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01
},
iconButton: {
padding: '2px',
'&:hover': {
backgroundColor: theme.palette.action03
}
}
};
};
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient<Props> {
class MessageRecipient extends AbstractMessageRecipient<IProps> {
/**
* Initializes a new {@code MessageRecipient} instance.
*
* @param {*} props - The read-only properties with which the new instance
* @param {IProps} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props) {
constructor(props: IProps) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this);
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
@ -37,7 +63,7 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
*
* @returns {void}
*/
_onKeyPress(e) {
_onKeyPress(e: React.KeyboardEvent) {
if (
(this.props._onRemovePrivateMessageRecipient || this.props._onHideLobbyChatRecipient)
&& (e.key === ' ' || e.key === 'Enter')
@ -64,11 +90,11 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
return null;
}
const { t } = this.props;
const { classes, t } = this.props;
return (
<div
className = { _isLobbyChatActive ? 'lobby-chat-recipient' : '' }
className = { classes.container }
id = 'chat-recipient'
role = 'alert'>
<span>
@ -76,19 +102,18 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
}) }
</span>
<div
aria-label = { t('dialog.close') }
<Button
accessibilityLabel = { t('dialog.close') }
className = { classes.iconButton }
icon = { IconCloseLarge }
onClick = { _isLobbyChatActive
? this.props._onHideLobbyChatRecipient : this.props._onRemovePrivateMessageRecipient }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
<Icon
src = { IconCloseCircle } />
</div>
type = { BUTTON_TYPES.TERTIARY } />
</div>
);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(
withStyles(styles)(MessageRecipient)));

View File

@ -1,99 +0,0 @@
// @flow
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { handleLobbyChatInitialized, openChat } from '../../actions';
export type Props = AbstractButtonProps & {
/**
* True if the message is a lobby chat message.
*/
isLobbyMessage: boolean,
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string,
/**
* True if the button is rendered as a reply button.
*/
reply: boolean,
/**
* Function to be used to translate i18n labels.
*/
t: Function,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The participant object retrieved from Redux.
*/
_participant: Object,
};
/**
* Class to render a button that initiates the sending of a private message through chet.
*/
class PrivateMessageButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
icon = IconMessage;
label = 'toolbar.privateMessage';
toggledIcon = IconReply;
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { _participant, participantID, dispatch, isLobbyMessage } = this.props;
if (isLobbyMessage) {
dispatch(handleLobbyChatInitialized(participantID));
} else {
dispatch(openChat(_participant));
}
}
/**
* Helper function to be implemented by subclasses, which must return a
* {@code boolean} value indicating if this button is toggled or not.
*
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.reply;
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_participant: getParticipantById(state, ownProps.participantID),
visible
};
}
export default translate(connect(_mapStateToProps)(PrivateMessageButton));

View File

@ -0,0 +1,72 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { CHAT_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { IconReply } from '../../../base/icons/svg';
import { getParticipantById } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { handleLobbyChatInitialized, openChat } from '../../actions.web';
interface IProps {
/**
* True if the message is a lobby chat message.
*/
isLobbyMessage: boolean;
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string;
/**
* Whether the button should be visible or not.
*/
visible?: boolean;
}
const useStyles = makeStyles()(theme => {
return {
replyButton: {
padding: '2px',
'&:hover': {
backgroundColor: theme.palette.action03
}
}
};
});
const PrivateMessageButton = ({ participantID, isLobbyMessage, visible }: IProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
const participant = useSelector((state: IReduxState) => getParticipantById(state, participantID));
const isVisible = useSelector((state: IReduxState) => getFeatureFlag(state, CHAT_ENABLED, true)) ?? visible;
const handleClick = useCallback(() => {
if (isLobbyMessage) {
dispatch(handleLobbyChatInitialized(participantID));
} else {
dispatch(openChat(participant));
}
}, []);
if (!isVisible) {
return null;
}
return (
<Button
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage'
className = { classes.replyButton }
icon = { IconReply }
onClick = { handleClick }
type = { BUTTON_TYPES.TERTIARY } />
);
};
export default PrivateMessageButton;

View File

@ -2,14 +2,15 @@
import React, { Component } from 'react';
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { IconMessage } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { openChat } from '../../../chat/';
import {
type Props as AbstractProps,
_mapStateToProps as _abstractMapStateToProps
type Props as AbstractProps
} from '../../../chat/components/web/PrivateMessageButton';
import { isButtonEnabled } from '../../../toolbox/functions.web';
@ -85,8 +86,12 @@ class PrivateMessageMenuButton extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { visible = enabled } = ownProps;
return {
..._abstractMapStateToProps(state, ownProps),
_participant: getParticipantById(state, ownProps.participantID),
visible,
_hidden: typeof interfaceConfig !== 'undefined'
&& (interfaceConfig.DISABLE_PRIVATE_MESSAGES || !isButtonEnabled('chat', state))
};