From 8e1d96cc48b2ed133682cf682a1e9e1a630fd02c Mon Sep 17 00:00:00 2001 From: robertpin Date: Tue, 17 Jan 2023 12:36:01 +0200 Subject: [PATCH] feat(chat) Redesign chat Move some styles from SCSS to JSS Convert some files to TS Implement redesign --- css/_chat.scss | 126 +--------- css/_variables.scss | 1 - package-lock.json | 19 ++ package.json | 1 + .../web/{Linkify.js => Linkify.tsx} | 12 +- .../web/{Message.js => Message.tsx} | 12 +- react/features/chat/actions.any.ts | 2 +- ...ChatMessage.js => AbstractChatMessage.tsx} | 35 ++- ...cipient.js => AbstractMessageRecipient.ts} | 61 +++-- .../chat/components/web/ChatMessage.js | 131 ---------- .../chat/components/web/ChatMessage.tsx | 223 ++++++++++++++++++ .../chat/components/web/ChatMessageGroup.js | 59 ----- .../chat/components/web/ChatMessageGroup.tsx | 81 +++++++ .../chat/components/web/MessageContainer.tsx | 1 - ...ssageRecipient.js => MessageRecipient.tsx} | 69 ++++-- .../components/web/PrivateMessageButton.js | 99 -------- .../components/web/PrivateMessageButton.tsx | 72 ++++++ .../web/PrivateMessageMenuButton.js | 11 +- 18 files changed, 507 insertions(+), 508 deletions(-) rename react/features/base/react/components/web/{Linkify.js => Linkify.tsx} (87%) rename react/features/base/react/components/web/{Message.js => Message.tsx} (91%) rename react/features/chat/components/{AbstractChatMessage.js => AbstractChatMessage.tsx} (81%) rename react/features/chat/components/{AbstractMessageRecipient.js => AbstractMessageRecipient.ts} (65%) delete mode 100644 react/features/chat/components/web/ChatMessage.js create mode 100644 react/features/chat/components/web/ChatMessage.tsx delete mode 100644 react/features/chat/components/web/ChatMessageGroup.js create mode 100644 react/features/chat/components/web/ChatMessageGroup.tsx rename react/features/chat/components/web/{MessageRecipient.js => MessageRecipient.tsx} (55%) delete mode 100644 react/features/chat/components/web/PrivateMessageButton.js create mode 100644 react/features/chat/components/web/PrivateMessageButton.tsx diff --git a/css/_chat.scss b/css/_chat.scss index 33fb7e0fb..3bddd35fb 100644 --- a/css/_chat.scss +++ b/css/_chat.scss @@ -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 { diff --git a/css/_variables.scss b/css/_variables.scss index b186e851a..f79323b14 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -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); diff --git a/package-lock.json b/package-lock.json index 270d472c7..72add6031 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cfd7fbd4a..9303b063a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/base/react/components/web/Linkify.js b/react/features/base/react/components/web/Linkify.tsx similarity index 87% rename from react/features/base/react/components/web/Linkify.js rename to react/features/base/react/components/web/Linkify.tsx index 57219f8a5..3090e0ed9 100644 --- a/react/features/base/react/components/web/Linkify.js +++ b/react/features/base/react/components/web/Linkify.tsx @@ -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 { +export default class Linkify extends Component { /** * Implements {@Component#render}. * diff --git a/react/features/base/react/components/web/Message.js b/react/features/base/react/components/web/Message.tsx similarity index 91% rename from react/features/base/react/components/web/Message.js rename to react/features/base/react/components/web/Message.tsx index 8ce84075f..81a013309 100644 --- a/react/features/base/react/components/web/Message.js +++ b/react/features/base/react/components/web/Message.tsx @@ -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 { */ _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 { return message; } - _processMessage: () => Array>; - /** * Implements React's {@link Component#render()}. * diff --git a/react/features/chat/actions.any.ts b/react/features/chat/actions.any.ts index e36901f10..a3935a210 100644 --- a/react/features/chat/actions.any.ts +++ b/react/features/chat/actions.any.ts @@ -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 diff --git a/react/features/chat/components/AbstractChatMessage.js b/react/features/chat/components/AbstractChatMessage.tsx similarity index 81% rename from react/features/chat/components/AbstractChatMessage.js rename to react/features/chat/components/AbstractChatMessage.tsx index e6b336411..d9afc28da 100644 --- a/react/features/chat/components/AbstractChatMessage.js +++ b/react/features/chat/components/AbstractChatMessage.tsx @@ -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 extends PureComponent

{ +export default class AbstractChatMessage

extends PureComponent

{ /** * Returns the timestamp to display for the message. * diff --git a/react/features/chat/components/AbstractMessageRecipient.js b/react/features/chat/components/AbstractMessageRecipient.ts similarity index 65% rename from react/features/chat/components/AbstractMessageRecipient.js rename to react/features/chat/components/AbstractMessageRecipient.ts index 87aea75af..b7e27a627 100644 --- a/react/features/chat/components/AbstractMessageRecipient.js +++ b/react/features/chat/components/AbstractMessageRecipient.ts @@ -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 extends PureComponent

{ +export default class AbstractMessageRecipient

extends PureComponent

{ } @@ -55,9 +52,9 @@ export default class AbstractMessageRecipient extends PureComponent

* 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 { +export function _mapDispatchToProps(dispatch: Function) { return { _onRemovePrivateMessageRecipient: () => { dispatch(setPrivateMessageRecipient()); @@ -72,9 +69,9 @@ export function _mapDispatchToProps(dispatch: Function): $Shape { * 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 { +export function _mapStateToProps(state: IReduxState) { const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat']; return { diff --git a/react/features/chat/components/web/ChatMessage.js b/react/features/chat/components/web/ChatMessage.js deleted file mode 100644 index 643f1e670..000000000 --- a/react/features/chat/components/web/ChatMessage.js +++ /dev/null @@ -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 { - /** - * Implements React's {@link Component#render()}. - * - * @inheritdoc - * @returns {ReactElement} - */ - render() { - const { message, t, knocking } = this.props; - - return ( -

-
-
-
- { this.props.showDisplayName && this._renderDisplayName() } -
- - { this.props.message.displayName === this.props.message.recipient - ? t('chat.messageAccessibleTitleMe') - : t('chat.messageAccessibleTitle', - { user: this.props.message.displayName }) } - - -
- { (message.privateMessage || (message.lobbyChat && !knocking)) - && this._renderPrivateNotice() } -
- { (message.privateMessage || (message.lobbyChat && !knocking)) - && message.messageType !== MESSAGE_TYPE_LOCAL - && ( -
- -
- ) } -
-
- { this.props.showTimestamp && this._renderTimestamp() } -
- ); - } - - _getFormattedTimestamp: () => string; - - _getMessageText: () => string; - - _getPrivateNoticeMessage: () => string; - - /** - * Renders the display name of the sender. - * - * @returns {React$Element<*>} - */ - _renderDisplayName() { - return ( -
- { this.props.message.displayName } -
- ); - } - - /** - * Renders the message privacy notice. - * - * @returns {React$Element<*>} - */ - _renderPrivateNotice() { - return ( -
- { this._getPrivateNoticeMessage() } -
- ); - } - - /** - * Renders the time at which the message was sent. - * - * @returns {React$Element<*>} - */ - _renderTimestamp() { - return ( -
- { this._getFormattedTimestamp() } -
- ); - } -} - -/** - * 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 { - const { knocking } = state['features/lobby']; - - return { - knocking - }; -} - -export default translate(connect(_mapStateToProps)(ChatMessage)); diff --git a/react/features/chat/components/web/ChatMessage.tsx b/react/features/chat/components/web/ChatMessage.tsx new file mode 100644 index 000000000..b9f840ac4 --- /dev/null +++ b/react/features/chat/components/web/ChatMessage.tsx @@ -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 { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { message, t, knocking, classes, type } = this.props; + + return ( +
+
+
+
+ { this.props.showDisplayName && this._renderDisplayName() } +
+ + { this.props.message.displayName === this.props.message.recipient + ? t('chat.messageAccessibleTitleMe') + : t('chat.messageAccessibleTitle', + { user: this.props.message.displayName }) } + + +
+ { (message.privateMessage || (message.lobbyChat && !knocking)) + && this._renderPrivateNotice() } +
+ { (message.privateMessage || (message.lobbyChat && !knocking)) + && message.messageType !== MESSAGE_TYPE_LOCAL + && ( +
+ +
+ ) } +
+
+ { this.props.showTimestamp && this._renderTimestamp() } +
+ ); + } + + /** + * Renders the display name of the sender. + * + * @returns {React$Element<*>} + */ + _renderDisplayName() { + return ( +
+ { this.props.message.displayName } +
+ ); + } + + /** + * Renders the message privacy notice. + * + * @returns {React$Element<*>} + */ + _renderPrivateNotice() { + return ( +
+ { this._getPrivateNoticeMessage() } +
+ ); + } + + /** + * Renders the time at which the message was sent. + * + * @returns {React$Element<*>} + */ + _renderTimestamp() { + return ( +
+ { this._getFormattedTimestamp() } +
+ ); + } +} + +/** + * 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))); diff --git a/react/features/chat/components/web/ChatMessageGroup.js b/react/features/chat/components/web/ChatMessageGroup.js deleted file mode 100644 index eaa12cc90..000000000 --- a/react/features/chat/components/web/ChatMessageGroup.js +++ /dev/null @@ -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, -}; - -/** - * 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 { - 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 ( -
- { messages.map((message, i) => ( - - ))} -
- ); - } -} - -export default ChatMessageGroup; diff --git a/react/features/chat/components/web/ChatMessageGroup.tsx b/react/features/chat/components/web/ChatMessageGroup.tsx new file mode 100644 index 000000000..c67d1da88 --- /dev/null +++ b/react/features/chat/components/web/ChatMessageGroup.tsx @@ -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; +} + +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 ( +
+ +
+ {messages.map((message, i) => ( + + ))} +
+
+ ); +}; + +export default ChatMessageGroup; diff --git a/react/features/chat/components/web/MessageContainer.tsx b/react/features/chat/components/web/MessageContainer.tsx index c1038c452..ccd38345e 100644 --- a/react/features/chat/components/web/MessageContainer.tsx +++ b/react/features/chat/components/web/MessageContainer.tsx @@ -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'; diff --git a/react/features/chat/components/web/MessageRecipient.js b/react/features/chat/components/web/MessageRecipient.tsx similarity index 55% rename from react/features/chat/components/web/MessageRecipient.js rename to react/features/chat/components/web/MessageRecipient.tsx index 8defb84ff..ccf9e0035 100644 --- a/react/features/chat/components/web/MessageRecipient.js +++ b/react/features/chat/components/web/MessageRecipient.tsx @@ -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 { +class MessageRecipient extends AbstractMessageRecipient { /** * 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 { * * @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 { return null; } - const { t } = this.props; + const { classes, t } = this.props; return ( ); } } -export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient)); +export default translate(connect(_mapStateToProps, _mapDispatchToProps)( + withStyles(styles)(MessageRecipient))); diff --git a/react/features/chat/components/web/PrivateMessageButton.js b/react/features/chat/components/web/PrivateMessageButton.js deleted file mode 100644 index b529b07d6..000000000 --- a/react/features/chat/components/web/PrivateMessageButton.js +++ /dev/null @@ -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 { - 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 { - const enabled = getFeatureFlag(state, CHAT_ENABLED, true); - const { visible = enabled } = ownProps; - - return { - _participant: getParticipantById(state, ownProps.participantID), - visible - }; -} - -export default translate(connect(_mapStateToProps)(PrivateMessageButton)); diff --git a/react/features/chat/components/web/PrivateMessageButton.tsx b/react/features/chat/components/web/PrivateMessageButton.tsx new file mode 100644 index 000000000..d4423c79b --- /dev/null +++ b/react/features/chat/components/web/PrivateMessageButton.tsx @@ -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 ( +