ref(chat): Refactor Chat components (#13550)

Remove Abstract component
Convert web component to function component
pull/13555/head
Robert Pintilii 2 years ago committed by GitHub
parent 398e170e2d
commit 824cfc0c9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 169
      react/features/chat/components/AbstractChat.ts
  2. 2
      react/features/chat/components/AbstractChatMessage.tsx
  3. 2
      react/features/chat/components/AbstractMessageContainer.ts
  4. 59
      react/features/chat/components/native/Chat.tsx
  5. 2
      react/features/chat/components/native/ChatMessageGroup.tsx
  6. 2
      react/features/chat/components/native/MessageContainer.tsx
  7. 249
      react/features/chat/components/web/Chat.tsx
  8. 2
      react/features/chat/components/web/ChatMessageGroup.tsx
  9. 2
      react/features/chat/functions.ts
  10. 15
      react/features/chat/reducer.ts
  11. 38
      react/features/chat/types.ts
  12. 2
      react/features/lobby/components/AbstractLobbyScreen.tsx

@ -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<P extends IProps> extends Component<P> {
/**
* 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<Object>,
* _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
};
}

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

@ -1,6 +1,6 @@
import { Component } from 'react';
import { IMessage } from '../reducer';
import { IMessage } from '../types';
export interface IProps {

@ -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<IProps> {
class Chat extends Component<IProps> {
/**
* 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,39 @@ class Chat extends AbstractChat<IProps> {
</JitsiScreen>
);
}
/**
* 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 {{
* _messages: Array<Object>,
* _nbUnreadMessages: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { messages, nbUnreadMessages } = state['features/chat'];
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {

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

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

@ -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<IProps> {
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 ? <div
className = 'sideToolbarContainer'
id = 'sideToolbarContainer'
onKeyDown = { this._onEscClick } >
<ChatHeader
className = 'chat-header'
isPollsEnabled = { _isPollsEnabled }
onCancel = { this._onToggleChat } />
{ _showNamePrompt
? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
: this._renderChat() }
</div> : 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<IProps> {
* @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();
}
}
}, [ _isOpen ]);
/**
* 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<IProps> {
* @private
* @returns {ReactElement}
*/
_renderChat() {
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
function _renderChat() {
return (
<>
{ _isPollsEnabled && this._renderTabs() }
{_isPollsEnabled && _renderTabs()}
<div
aria-labelledby = { CHAT_TABS.CHAT }
className = { clsx(
@ -141,12 +160,12 @@ class Chat extends AbstractChat<IProps> {
role = 'tabpanel'
tabIndex = { 0 }>
<MessageContainer
messages = { this.props._messages } />
messages = { _messages } />
<MessageRecipient />
<ChatInput
onSend = { this._onSendMessage } />
onSend = { onSendMessage } />
</div>
{ _isPollsEnabled && (
{_isPollsEnabled && (
<>
<div
aria-labelledby = { CHAT_TABS.POLLS }
@ -169,13 +188,11 @@ class Chat extends AbstractChat<IProps> {
* @private
* @returns {ReactElement}
*/
_renderTabs() {
const { _isPollsEnabled, _isPollsTabFocused, _nbUnreadMessages, _nbUnreadPolls, t } = this.props;
function _renderTabs() {
return (
<Tabs
accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
onChange = { this._onChangeTab }
onChange = { _onChangeTab }
selected = { _isPollsTabFocused ? CHAT_TABS.POLLS : CHAT_TABS.CHAT }
tabs = { [ {
accessibilityLabel: t('chat.tabs.chat'),
@ -194,24 +211,56 @@ class Chat extends AbstractChat<IProps> {
);
}
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
return (
_isOpen ? <div
className = 'sideToolbarContainer'
id = 'sideToolbarContainer'
onKeyDown = { _onEscClick } >
<ChatHeader
className = 'chat-header'
isPollsEnabled = { _isPollsEnabled }
onCancel = { onToggleChat } />
{_showNamePrompt
? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
: _renderChat()}
</div> : 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 {{
* _isModal: boolean,
* _isOpen: boolean,
* _isPollsEnabled: boolean,
* _isPollsTabFocused: boolean,
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _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));

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

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

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

@ -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'];
}

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

Loading…
Cancel
Save