feat(chat) Redesign chat

Move some styles from SCSS to JSS
Convert some files to TS
Implement redesign
pull/12823/head
robertpin 2 years ago committed by Saúl Ibarra Corretgé
parent e3166e6faa
commit 8e1d96cc48
  1. 126
      css/_chat.scss
  2. 1
      css/_variables.scss
  3. 19
      package-lock.json
  4. 1
      package.json
  5. 12
      react/features/base/react/components/web/Linkify.tsx
  6. 12
      react/features/base/react/components/web/Message.tsx
  7. 2
      react/features/chat/actions.any.ts
  8. 35
      react/features/chat/components/AbstractChatMessage.tsx
  9. 61
      react/features/chat/components/AbstractMessageRecipient.ts
  10. 131
      react/features/chat/components/web/ChatMessage.js
  11. 223
      react/features/chat/components/web/ChatMessage.tsx
  12. 59
      react/features/chat/components/web/ChatMessageGroup.js
  13. 81
      react/features/chat/components/web/ChatMessageGroup.tsx
  14. 1
      react/features/chat/components/web/MessageContainer.tsx
  15. 69
      react/features/chat/components/web/MessageRecipient.tsx
  16. 99
      react/features/chat/components/web/PrivateMessageButton.js
  17. 72
      react/features/chat/components/web/PrivateMessageButton.tsx
  18. 11
      react/features/video-menu/components/web/PrivateMessageMenuButton.js

@ -32,7 +32,7 @@
#chat-conversation-container { #chat-conversation-container {
// extract message input height // extract message input height
height: calc(100% - 68px); height: calc(100% - 64px);
overflow: hidden; overflow: hidden;
position: relative; 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 { .chat-header {
height: 70px; height: 70px;
@ -124,13 +98,12 @@
} }
.chat-input-container { .chat-input-container {
padding: 0 16px 16px; padding: 0 16px 24px;
} }
#chat-input { #chat-input {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
padding: 4px;
position: relative; position: relative;
} }
@ -263,15 +236,6 @@
-webkit-user-select: text; -webkit-user-select: text;
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 { .sr-only {
@ -288,24 +252,11 @@
} }
.chatmessage { .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 { &.localuser {
background-color: $chatLocalMessageBackgroundColor; background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px; border-radius: 6px 0px 6px 6px;
} }
.usermessage {
white-space: pre-wrap;
font-size: 14px;
}
&.error { &.error {
border-radius: 0px; border-radius: 0px;
@ -320,22 +271,12 @@
} }
} }
.privatemessagenotice {
font-size: 11px;
font-weight: 100;
}
.messagecontent { .messagecontent {
margin: 8px;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
} }
} }
.timestamp {
color: #757575;
}
#smileys { #smileys {
font-size: 20pt; font-size: 20pt;
margin: auto; margin: auto;
@ -409,24 +350,9 @@
} }
.chat-message-group { .chat-message-group {
display: flex;
flex-direction: column;
&.local { &.local {
align-items: flex-end; 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-name {
display: none; display: none;
} }
@ -437,58 +363,10 @@
} }
&.error { &.error {
.chatmessage {
background-color: $defaultWarningColor;
border-radius: 0px;
font-weight: 100;
}
.display-name { .display-name {
display: none; 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 { .chat-dialog {

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

19
package-lock.json generated

@ -147,6 +147,7 @@
"@types/js-md5": "0.4.3", "@types/js-md5": "0.4.3",
"@types/lodash": "4.14.182", "@types/lodash": "4.14.182",
"@types/react": "17.0.14", "@types/react": "17.0.14",
"@types/react-linkify": "1.0.1",
"@types/react-native": "0.68.9", "@types/react-native": "0.68.9",
"@types/react-redux": "7.1.24", "@types/react-redux": "7.1.24",
"@types/react-window": "1.8.5", "@types/react-window": "1.8.5",
@ -6539,6 +6540,15 @@
"@types/react": "*" "@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": { "node_modules/@types/react-native": {
"version": "0.68.9", "version": "0.68.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz",
@ -25265,6 +25275,15 @@
"@types/react": "*" "@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": { "@types/react-native": {
"version": "0.68.9", "version": "0.68.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.68.9.tgz",

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

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

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

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

@ -1,9 +1,9 @@
// @flow
import { PureComponent } from 'react'; 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 { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
import { IMessage } from '../reducer';
/** /**
* Formatter string to display the message timestamp. * 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}. * 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. * The representation of a chat message.
*/ */
message: Object, message: IMessage;
/** /**
* Whether or not the avatar image of the participant which sent the message * Whether or not the avatar image of the participant which sent the message
* should be displayed. * should be displayed.
*/ */
showAvatar: boolean, showAvatar?: boolean;
/** /**
* Whether or not the name of the participant which sent the message should * Whether or not the name of the participant which sent the message should
* be displayed. * be displayed.
*/ */
showDisplayName: boolean, showDisplayName: boolean;
/** /**
* Whether or not the time at which the message was sent should be * Whether or not the time at which the message was sent should be
* displayed. * displayed.
*/ */
showTimestamp: boolean, showTimestamp: boolean;
}
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean,
/**
* Invoked to receive translated strings.
*/
t: Function
};
/** /**
* Abstract component to display a chat message. * 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. * Returns the timestamp to display for the message.
* *

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

@ -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));

@ -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)));

@ -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;

@ -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;

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

@ -1,35 +1,61 @@
// @flow import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react'; import React from 'react';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n/functions';
import { Icon, IconCloseCircle } from '../../../base/icons'; import { IconCloseLarge } from '../../../base/icons/svg';
import { connect } from '../../../base/redux'; 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, { import AbstractMessageRecipient, {
type Props, IProps,
_mapDispatchToProps, _mapDispatchToProps,
_mapStateToProps _mapStateToProps
} from '../AbstractMessageRecipient'; } 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 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. * 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. * is to be initialized.
*/ */
constructor(props) { constructor(props: IProps) {
super(props); super(props);
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onKeyPress = this._onKeyPress.bind(this); this._onKeyPress = this._onKeyPress.bind(this);
} }
_onKeyPress: (Object) => void;
/** /**
* KeyPress handler for accessibility. * KeyPress handler for accessibility.
* *
@ -37,7 +63,7 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* *
* @returns {void} * @returns {void}
*/ */
_onKeyPress(e) { _onKeyPress(e: React.KeyboardEvent) {
if ( if (
(this.props._onRemovePrivateMessageRecipient || this.props._onHideLobbyChatRecipient) (this.props._onRemovePrivateMessageRecipient || this.props._onHideLobbyChatRecipient)
&& (e.key === ' ' || e.key === 'Enter') && (e.key === ' ' || e.key === 'Enter')
@ -64,11 +90,11 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
return null; return null;
} }
const { t } = this.props; const { classes, t } = this.props;
return ( return (
<div <div
className = { _isLobbyChatActive ? 'lobby-chat-recipient' : '' } className = { classes.container }
id = 'chat-recipient' id = 'chat-recipient'
role = 'alert'> role = 'alert'>
<span> <span>
@ -76,19 +102,18 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
}) } }) }
</span> </span>
<div <Button
aria-label = { t('dialog.close') } accessibilityLabel = { t('dialog.close') }
className = { classes.iconButton }
icon = { IconCloseLarge }
onClick = { _isLobbyChatActive onClick = { _isLobbyChatActive
? this.props._onHideLobbyChatRecipient : this.props._onRemovePrivateMessageRecipient } ? this.props._onHideLobbyChatRecipient : this.props._onRemovePrivateMessageRecipient }
onKeyPress = { this._onKeyPress } onKeyPress = { this._onKeyPress }
role = 'button' type = { BUTTON_TYPES.TERTIARY } />
tabIndex = { 0 }>
<Icon
src = { IconCloseCircle } />
</div>
</div> </div>
); );
} }
} }
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient)); export default translate(connect(_mapStateToProps, _mapDispatchToProps)(
withStyles(styles)(MessageRecipient)));

@ -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));

@ -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;

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

Loading…
Cancel
Save