From a0d1c8d7c8f906fb4624ae8644bac45bb26e03bc Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Tue, 11 Jul 2023 15:40:29 +0300 Subject: [PATCH 1/2] ref(chat): Refactor Chat components Remove Abstract component Convert web component to function component --- .../features/chat/components/AbstractChat.ts | 169 ------------ .../chat/components/AbstractChatMessage.tsx | 2 +- .../components/AbstractMessageContainer.ts | 2 +- .../features/chat/components/native/Chat.tsx | 60 ++++- .../components/native/ChatMessageGroup.tsx | 2 +- .../components/native/MessageContainer.tsx | 2 +- react/features/chat/components/web/Chat.tsx | 244 +++++++++++------- .../chat/components/web/ChatMessageGroup.tsx | 2 +- react/features/chat/functions.ts | 2 +- react/features/chat/reducer.ts | 15 +- react/features/chat/types.ts | 38 +++ .../lobby/components/AbstractLobbyScreen.tsx | 2 +- 12 files changed, 243 insertions(+), 297 deletions(-) delete mode 100644 react/features/chat/components/AbstractChat.ts create mode 100644 react/features/chat/types.ts diff --git a/react/features/chat/components/AbstractChat.ts b/react/features/chat/components/AbstractChat.ts deleted file mode 100644 index ec6b6e5103756..0000000000000 --- a/react/features/chat/components/AbstractChat.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Component } from 'react'; -import { WithTranslation } from 'react-i18next'; - -import { IReduxState, IStore } from '../../app/types'; -import { getLocalParticipant } from '../../base/participants/functions'; -import { sendMessage, setIsPollsTabFocused } from '../actions'; -import { SMALL_WIDTH_THRESHOLD } from '../constants'; -import { IMessage } from '../reducer'; - -/** - * The type of the React {@code Component} props of {@code AbstractChat}. - */ -export interface IProps extends WithTranslation { - - /** - * Whether the chat is opened in a modal or not (computed based on window width). - */ - _isModal: boolean; - - /** - * True if the chat window should be rendered. - */ - _isOpen: boolean; - - /** - * True if the polls feature is enabled. - */ - _isPollsEnabled: boolean; - - /** - * Whether the poll tab is focused or not. - */ - _isPollsTabFocused: boolean; - - /** - * All the chat messages in the conference. - */ - _messages: IMessage[]; - - /** - * Number of unread chat messages. - */ - _nbUnreadMessages: number; - - /** - * Number of unread poll messages. - */ - _nbUnreadPolls: number; - - /** - * Function to send a text message. - * - * @protected - */ - _onSendMessage: Function; - - /** - * Function to toggle the chat window. - */ - _onToggleChat: Function; - - /** - * Function to display the chat tab. - * - * @protected - */ - _onToggleChatTab: Function; - - /** - * Function to display the polls tab. - * - * @protected - */ - _onTogglePollsTab: Function; - - /** - * Whether or not to block chat access with a nickname input form. - */ - _showNamePrompt: boolean; - - /** - * The Redux dispatch function. - */ - dispatch: IStore['dispatch']; -} - -/** - * Implements an abstract chat panel. - */ -export default class AbstractChat

extends Component

{ - - /** - * Initializes a new {@code AbstractChat} instance. - * - * @param {Props} props - The React {@code Component} props to initialize - * the new {@code AbstractChat} instance with. - */ - constructor(props: P) { - super(props); - - // Bind event handlers so they are only bound once per instance. - this._onSendMessage = this._onSendMessage.bind(this); - this._onToggleChatTab = this._onToggleChatTab.bind(this); - this._onTogglePollsTab = this._onTogglePollsTab.bind(this); - } - - /** - * Sends a text message. - * - * @private - * @param {string} text - The text message to be sent. - * @returns {void} - * @type {Function} - */ - _onSendMessage(text: string) { - this.props.dispatch(sendMessage(text)); - } - - /** - * Display the Chat tab. - * - * @private - * @returns {void} - */ - _onToggleChatTab() { - this.props.dispatch(setIsPollsTabFocused(false)); - } - - /** - * Display the Polls tab. - * - * @private - * @returns {void} - */ - _onTogglePollsTab() { - this.props.dispatch(setIsPollsTabFocused(true)); - } -} - -/** - * Maps (parts of) the redux state to {@link Chat} React {@code Component} - * props. - * - * @param {Object} state - The redux store/state. - * @param {any} _ownProps - Components' own props. - * @private - * @returns {{ - * _isOpen: boolean, - * _messages: Array, - * _showNamePrompt: boolean - * }} - */ -export function _mapStateToProps(state: IReduxState, _ownProps: any) { - const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat']; - const { nbUnreadPolls } = state['features/polls']; - const _localParticipant = getLocalParticipant(state); - const { disablePolls } = state['features/base/config']; - - return { - _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD, - _isOpen: isOpen, - _isPollsEnabled: !disablePolls, - _isPollsTabFocused: isPollsTabFocused, - _messages: messages, - _nbUnreadMessages: nbUnreadMessages, - _nbUnreadPolls: nbUnreadPolls, - _showNamePrompt: !_localParticipant?.name - }; -} diff --git a/react/features/chat/components/AbstractChatMessage.tsx b/react/features/chat/components/AbstractChatMessage.tsx index d9afc28dae352..374280d50aa07 100644 --- a/react/features/chat/components/AbstractChatMessage.tsx +++ b/react/features/chat/components/AbstractChatMessage.tsx @@ -3,7 +3,7 @@ import { WithTranslation } from 'react-i18next'; import { getLocalizedDateFormatter } from '../../base/i18n/dateUtil'; import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants'; -import { IMessage } from '../reducer'; +import { IMessage } from '../types'; /** * Formatter string to display the message timestamp. diff --git a/react/features/chat/components/AbstractMessageContainer.ts b/react/features/chat/components/AbstractMessageContainer.ts index b745ffcf710b1..d5a40b3f5b3ca 100644 --- a/react/features/chat/components/AbstractMessageContainer.ts +++ b/react/features/chat/components/AbstractMessageContainer.ts @@ -1,6 +1,6 @@ import { Component } from 'react'; -import { IMessage } from '../reducer'; +import { IMessage } from '../types'; export interface IProps { diff --git a/react/features/chat/components/native/Chat.tsx b/react/features/chat/components/native/Chat.tsx index 84991a9dbe0b4..9ce02c899dbef 100644 --- a/react/features/chat/components/native/Chat.tsx +++ b/react/features/chat/components/native/Chat.tsx @@ -1,16 +1,14 @@ /* eslint-disable react/no-multi-comp */ import { Route, useIsFocused } from '@react-navigation/native'; -import React, { useEffect } from 'react'; +import React, { Component, useEffect } from 'react'; import { connect } from 'react-redux'; +import { IReduxState } from '../../../app/types'; import { translate } from '../../../base/i18n/functions'; import JitsiScreen from '../../../base/modal/components/JitsiScreen'; import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter'; -import { closeChat } from '../../actions.native'; -import AbstractChat, { - IProps as AbstractProps, - _mapStateToProps -} from '../AbstractChat'; +import { closeChat, sendMessage } from '../../actions.native'; +import { IProps as AbstractProps } from '../../types'; import ChatInputBar from './ChatInputBar'; import MessageContainer from './MessageContainer'; @@ -34,7 +32,21 @@ interface IProps extends AbstractProps { * Implements a React native component that renders the chat window (modal) of * the mobile client. */ -class Chat extends AbstractChat { +class Chat extends Component { + + /** + * Initializes a new {@code AbstractChat} instance. + * + * @param {Props} props - The React {@code Component} props to initialize + * the new {@code AbstractChat} instance with. + */ + constructor(props: IProps) { + super(props); + + // Bind event handlers so they are only bound once per instance. + this._onSendMessage = this._onSendMessage.bind(this); + } + /** * Implements React's {@link Component#render()}. * @@ -57,6 +69,40 @@ class Chat extends AbstractChat { ); } + + /** + * Sends a text message. + * + * @private + * @param {string} text - The text message to be sent. + * @returns {void} + * @type {Function} + */ + _onSendMessage(text: string) { + this.props.dispatch(sendMessage(text)); + } +} + +/** + * Maps (parts of) the redux state to {@link Chat} React {@code Component} + * props. + * + * @param {Object} state - The redux store/state. + * @param {any} _ownProps - Components' own props. + * @private + * @returns {{ + * _isOpen: boolean, + * _messages: Array, + * _showNamePrompt: boolean + * }} + */ +function _mapStateToProps(state: IReduxState, _ownProps: any) { + const { messages, nbUnreadMessages } = state['features/chat']; + + return { + _messages: messages, + _nbUnreadMessages: nbUnreadMessages + }; } export default translate(connect(_mapStateToProps)((props: IProps) => { diff --git a/react/features/chat/components/native/ChatMessageGroup.tsx b/react/features/chat/components/native/ChatMessageGroup.tsx index 25e73e8d60282..9b29e76e70a61 100644 --- a/react/features/chat/components/native/ChatMessageGroup.tsx +++ b/react/features/chat/components/native/ChatMessageGroup.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { FlatList } from 'react-native'; import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants'; -import { IMessage } from '../../reducer'; +import { IMessage } from '../../types'; import ChatMessage from './ChatMessage'; diff --git a/react/features/chat/components/native/MessageContainer.tsx b/react/features/chat/components/native/MessageContainer.tsx index 8884d6ba3cb83..a63150e6b92e9 100644 --- a/react/features/chat/components/native/MessageContainer.tsx +++ b/react/features/chat/components/native/MessageContainer.tsx @@ -3,7 +3,7 @@ import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native'; import { connect } from 'react-redux'; import { translate } from '../../../base/i18n/functions'; -import { IMessage } from '../../reducer'; +import { IMessage } from '../../types'; import AbstractMessageContainer, { IProps as AbstractProps } from '../AbstractMessageContainer'; import ChatMessageGroup from './ChatMessageGroup'; diff --git a/react/features/chat/components/web/Chat.tsx b/react/features/chat/components/web/Chat.tsx index ef69929b3aac2..475baaa53652e 100644 --- a/react/features/chat/components/web/Chat.tsx +++ b/react/features/chat/components/web/Chat.tsx @@ -1,16 +1,15 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; +import { IReduxState } from '../../../app/types'; import { translate } from '../../../base/i18n/functions'; +import { getLocalParticipant } from '../../../base/participants/functions'; import Tabs from '../../../base/ui/components/web/Tabs'; import PollsPane from '../../../polls/components/web/PollsPane'; -import { toggleChat } from '../../actions.web'; -import { CHAT_TABS } from '../../constants'; -import AbstractChat, { - IProps, - _mapStateToProps -} from '../AbstractChat'; +import { sendMessage, setIsPollsTabFocused, toggleChat } from '../../actions.web'; +import { CHAT_TABS, SMALL_WIDTH_THRESHOLD } from '../../constants'; +import { IProps as AbstractProps } from '../../types'; import ChatHeader from './ChatHeader'; import ChatInput from './ChatInput'; @@ -19,75 +18,101 @@ import KeyboardAvoider from './KeyboardAvoider'; import MessageContainer from './MessageContainer'; import MessageRecipient from './MessageRecipient'; -/** - * React Component for holding the chat feature in a side panel that slides in - * and out of view. - */ -class Chat extends AbstractChat { +interface IProps extends AbstractProps { /** - * Reference to the React Component for displaying chat messages. Used for - * scrolling to the end of the chat messages. + * Whether the chat is opened in a modal or not (computed based on window width). */ - _messageContainerRef: Object; + _isModal: boolean; /** - * Initializes a new {@code Chat} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. + * True if the chat window should be rendered. */ - constructor(props: IProps) { - super(props); + _isOpen: boolean; - this._messageContainerRef = React.createRef(); + /** + * True if the polls feature is enabled. + */ + _isPollsEnabled: boolean; - // Bind event handlers so they are only bound once for every instance. - this._onChatTabKeyDown = this._onChatTabKeyDown.bind(this); - this._onEscClick = this._onEscClick.bind(this); - this._onPollsTabKeyDown = this._onPollsTabKeyDown.bind(this); - this._onToggleChat = this._onToggleChat.bind(this); - this._onChangeTab = this._onChangeTab.bind(this); - } + /** + * Whether the poll tab is focused or not. + */ + _isPollsTabFocused: boolean; /** - * Implements React's {@link Component#render()}. + * Number of unread poll messages. + */ + _nbUnreadPolls: number; + + /** + * Function to send a text message. * - * @inheritdoc - * @returns {ReactElement} + * @protected */ - render() { - const { _isOpen, _isPollsEnabled, _showNamePrompt } = this.props; + _onSendMessage: Function; - return ( - _isOpen ?
- - { _showNamePrompt - ? - : this._renderChat() } -
: null - ); - } + /** + * Function to toggle the chat window. + */ + _onToggleChat: Function; /** - * Key press handler for the chat tab. + * Function to display the chat tab. * - * @param {KeyboardEvent} event - The event. - * @returns {void} + * @protected */ - _onChatTabKeyDown(event: React.KeyboardEvent) { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - this._onToggleChatTab(); - } - } + _onToggleChatTab: Function; + + /** + * Function to display the polls tab. + * + * @protected + */ + _onTogglePollsTab: Function; + + /** + * Whether or not to block chat access with a nickname input form. + */ + _showNamePrompt: boolean; +} + +const Chat = ({ + _isModal, + _isOpen, + _isPollsEnabled, + _isPollsTabFocused, + _messages, + _nbUnreadMessages, + _nbUnreadPolls, + _onSendMessage, + _onToggleChat, + _onToggleChatTab, + _onTogglePollsTab, + _showNamePrompt, + dispatch, + t +}: IProps) => { + /** + * Sends a text message. + * + * @private + * @param {string} text - The text message to be sent. + * @returns {void} + * @type {Function} + */ + const onSendMessage = useCallback((text: string) => { + dispatch(sendMessage(text)); + }, []); + + /** + * Toggles the chat window. + * + * @returns {Function} + */ + const onToggleChat = useCallback(() => { + dispatch(toggleChat()); + }, []); /** * Click handler for the chat sidenav. @@ -95,27 +120,23 @@ class Chat extends AbstractChat { * @param {KeyboardEvent} event - Esc key click to close the popup. * @returns {void} */ - _onEscClick(event: React.KeyboardEvent) { - if (event.key === 'Escape' && this.props._isOpen) { + const _onEscClick = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'Escape' && _isOpen) { event.preventDefault(); event.stopPropagation(); - this._onToggleChat(); + onToggleChat(); } - } + }, []); /** - * Key press handler for the polls tab. + * Change selected tab. * - * @param {KeyboardEvent} event - The event. + * @param {string} id - Id of the clicked tab. * @returns {void} */ - _onPollsTabKeyDown(event: React.KeyboardEvent) { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - this._onTogglePollsTab(); - } - } + const _onChangeTab = useCallback((id: string) => { + dispatch(setIsPollsTabFocused(id !== CHAT_TABS.CHAT)); + }, []); /** * Returns a React Element for showing chat messages and a form to send new @@ -124,12 +145,10 @@ class Chat extends AbstractChat { * @private * @returns {ReactElement} */ - _renderChat() { - const { _isPollsEnabled, _isPollsTabFocused } = this.props; - + function _renderChat() { return ( <> - { _isPollsEnabled && this._renderTabs() } + {_isPollsEnabled && _renderTabs()}
{ role = 'tabpanel' tabIndex = { 0 }> + messages = { _messages } /> + onSend = { onSendMessage } />
- { _isPollsEnabled && ( + {_isPollsEnabled && ( <>
{ * @private * @returns {ReactElement} */ - _renderTabs() { - const { _isPollsEnabled, _isPollsTabFocused, _nbUnreadMessages, _nbUnreadPolls, t } = this.props; - + function _renderTabs() { return ( { ); } - /** - * Toggles the chat window. - * - * @returns {Function} - */ - _onToggleChat() { - this.props.dispatch(toggleChat()); - } + return ( + _isOpen ?
+ + {_showNamePrompt + ? + : _renderChat()} +
: null + ); +}; - /** - * Change selected tab. - * - * @param {string} id - Id of the clicked tab. - * @returns {void} - */ - _onChangeTab(id: string) { - id === CHAT_TABS.CHAT ? this._onToggleChatTab() : this._onTogglePollsTab(); - } +/** + * Maps (parts of) the redux state to {@link Chat} React {@code Component} + * props. + * + * @param {Object} state - The redux store/state. + * @param {any} _ownProps - Components' own props. + * @private + * @returns {{ + * _isOpen: boolean, + * _messages: Array, + * _showNamePrompt: boolean + * }} + */ +function _mapStateToProps(state: IReduxState, _ownProps: any) { + const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat']; + const { nbUnreadPolls } = state['features/polls']; + const _localParticipant = getLocalParticipant(state); + const { disablePolls } = state['features/base/config']; + + return { + _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD, + _isOpen: isOpen, + _isPollsEnabled: !disablePolls, + _isPollsTabFocused: isPollsTabFocused, + _messages: messages, + _nbUnreadMessages: nbUnreadMessages, + _nbUnreadPolls: nbUnreadPolls, + _showNamePrompt: !_localParticipant?.name + }; } export default translate(connect(_mapStateToProps)(Chat)); diff --git a/react/features/chat/components/web/ChatMessageGroup.tsx b/react/features/chat/components/web/ChatMessageGroup.tsx index 4c2ff05406464..b156712e11d8e 100644 --- a/react/features/chat/components/web/ChatMessageGroup.tsx +++ b/react/features/chat/components/web/ChatMessageGroup.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { makeStyles } from 'tss-react/mui'; import Avatar from '../../../base/avatar/components/Avatar'; -import { IMessage } from '../../reducer'; +import { IMessage } from '../../types'; import ChatMessage from './ChatMessage'; diff --git a/react/features/chat/functions.ts b/react/features/chat/functions.ts index e89bbfb70c76b..046dee2cda006 100644 --- a/react/features/chat/functions.ts +++ b/react/features/chat/functions.ts @@ -7,7 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases'; import { IReduxState } from '../app/types'; import { escapeRegexp } from '../base/util/helpers'; -import { IMessage } from './reducer'; +import { IMessage } from './types'; /** * An ASCII emoticon regexp array to find and replace old-style ASCII diff --git a/react/features/chat/reducer.ts b/react/features/chat/reducer.ts index 83e61daf4dbed..d5c63332d8b0f 100644 --- a/react/features/chat/reducer.ts +++ b/react/features/chat/reducer.ts @@ -15,6 +15,7 @@ import { SET_LOBBY_CHAT_RECIPIENT, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes'; +import { IMessage } from './types'; const DEFAULT_STATE = { isOpen: false, @@ -27,20 +28,6 @@ const DEFAULT_STATE = { isLobbyChatActive: false }; -export interface IMessage { - displayName: string; - error?: Object; - id: string; - isReaction: boolean; - lobbyChat: boolean; - message: string; - messageId: string; - messageType: string; - privateMessage: boolean; - recipient: string; - timestamp: number; -} - export interface IChatState { isLobbyChatActive: boolean; isOpen: boolean; diff --git a/react/features/chat/types.ts b/react/features/chat/types.ts new file mode 100644 index 0000000000000..eecb9081a3d47 --- /dev/null +++ b/react/features/chat/types.ts @@ -0,0 +1,38 @@ +import { WithTranslation } from 'react-i18next'; + +import { IStore } from '../app/types'; + +export interface IMessage { + displayName: string; + error?: Object; + id: string; + isReaction: boolean; + lobbyChat: boolean; + message: string; + messageId: string; + messageType: string; + privateMessage: boolean; + recipient: string; + timestamp: number; +} + +/** + * The type of the React {@code Component} props of {@code AbstractChat}. + */ +export interface IProps extends WithTranslation { + + /** + * All the chat messages in the conference. + */ + _messages: IMessage[]; + + /** + * Number of unread chat messages. + */ + _nbUnreadMessages: number; + + /** + * The Redux dispatch function. + */ + dispatch: IStore['dispatch']; +} diff --git a/react/features/lobby/components/AbstractLobbyScreen.tsx b/react/features/lobby/components/AbstractLobbyScreen.tsx index a31f0b2337fff..c0575ad639f96 100644 --- a/react/features/lobby/components/AbstractLobbyScreen.tsx +++ b/react/features/lobby/components/AbstractLobbyScreen.tsx @@ -10,7 +10,7 @@ import { getFeatureFlag } from '../../base/flags/functions'; import { getLocalParticipant } from '../../base/participants/functions'; import { getFieldValue } from '../../base/react/functions'; import { updateSettings } from '../../base/settings/actions'; -import { IMessage } from '../../chat/reducer'; +import { IMessage } from '../../chat/types'; import { isDeviceStatusVisible } from '../../prejoin/functions'; import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions'; From 5aa6ea2f3f1496c3ae288c437d86d9a9e3b1e86f Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Wed, 12 Jul 2023 11:56:30 +0300 Subject: [PATCH 2/2] Code review --- react/features/chat/components/native/Chat.tsx | 3 +-- react/features/chat/components/web/Chat.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/react/features/chat/components/native/Chat.tsx b/react/features/chat/components/native/Chat.tsx index 9ce02c899dbef..879813a6a85a0 100644 --- a/react/features/chat/components/native/Chat.tsx +++ b/react/features/chat/components/native/Chat.tsx @@ -91,9 +91,8 @@ class Chat extends Component { * @param {any} _ownProps - Components' own props. * @private * @returns {{ - * _isOpen: boolean, * _messages: Array, - * _showNamePrompt: boolean + * _nbUnreadMessages: number * }} */ function _mapStateToProps(state: IReduxState, _ownProps: any) { diff --git a/react/features/chat/components/web/Chat.tsx b/react/features/chat/components/web/Chat.tsx index 475baaa53652e..03457c3323e96 100644 --- a/react/features/chat/components/web/Chat.tsx +++ b/react/features/chat/components/web/Chat.tsx @@ -126,7 +126,7 @@ const Chat = ({ event.stopPropagation(); onToggleChat(); } - }, []); + }, [ _isOpen ]); /** * Change selected tab. @@ -235,8 +235,13 @@ const Chat = ({ * @param {any} _ownProps - Components' own props. * @private * @returns {{ + * _isModal: boolean, * _isOpen: boolean, + * _isPollsEnabled: boolean, + * _isPollsTabFocused: boolean, * _messages: Array, + * _nbUnreadMessages: number, + * _nbUnreadPolls: number, * _showNamePrompt: boolean * }} */