You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Chat.tsx 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import React, { useCallback } from 'react';
  2. import { connect } from 'react-redux';
  3. import { makeStyles } from 'tss-react/mui';
  4. import { IReduxState } from '../../../app/types';
  5. import { translate } from '../../../base/i18n/functions';
  6. import { getLocalParticipant } from '../../../base/participants/functions';
  7. import { withPixelLineHeight } from '../../../base/styles/functions.web';
  8. import Tabs from '../../../base/ui/components/web/Tabs';
  9. import { arePollsDisabled } from '../../../conference/functions.any';
  10. import PollsPane from '../../../polls/components/web/PollsPane';
  11. import { sendMessage, setIsPollsTabFocused, toggleChat } from '../../actions.web';
  12. import { CHAT_SIZE, CHAT_TABS, SMALL_WIDTH_THRESHOLD } from '../../constants';
  13. import { IChatProps as AbstractProps } from '../../types';
  14. import ChatHeader from './ChatHeader';
  15. import ChatInput from './ChatInput';
  16. import DisplayNameForm from './DisplayNameForm';
  17. import KeyboardAvoider from './KeyboardAvoider';
  18. import MessageContainer from './MessageContainer';
  19. import MessageRecipient from './MessageRecipient';
  20. interface IProps extends AbstractProps {
  21. /**
  22. * Whether the chat is opened in a modal or not (computed based on window width).
  23. */
  24. _isModal: boolean;
  25. /**
  26. * True if the chat window should be rendered.
  27. */
  28. _isOpen: boolean;
  29. /**
  30. * True if the polls feature is enabled.
  31. */
  32. _isPollsEnabled: boolean;
  33. /**
  34. * Whether the poll tab is focused or not.
  35. */
  36. _isPollsTabFocused: boolean;
  37. /**
  38. * Number of unread poll messages.
  39. */
  40. _nbUnreadPolls: number;
  41. /**
  42. * Function to send a text message.
  43. *
  44. * @protected
  45. */
  46. _onSendMessage: Function;
  47. /**
  48. * Function to toggle the chat window.
  49. */
  50. _onToggleChat: Function;
  51. /**
  52. * Function to display the chat tab.
  53. *
  54. * @protected
  55. */
  56. _onToggleChatTab: Function;
  57. /**
  58. * Function to display the polls tab.
  59. *
  60. * @protected
  61. */
  62. _onTogglePollsTab: Function;
  63. /**
  64. * Whether or not to block chat access with a nickname input form.
  65. */
  66. _showNamePrompt: boolean;
  67. }
  68. const useStyles = makeStyles()(theme => {
  69. return {
  70. container: {
  71. backgroundColor: theme.palette.ui01,
  72. flexShrink: 0,
  73. overflow: 'hidden',
  74. position: 'relative',
  75. transition: 'width .16s ease-in-out',
  76. width: `${CHAT_SIZE}px`,
  77. zIndex: 300,
  78. '@media (max-width: 580px)': {
  79. height: '100dvh',
  80. position: 'fixed',
  81. left: 0,
  82. right: 0,
  83. top: 0,
  84. width: 'auto'
  85. },
  86. '*': {
  87. userSelect: 'text',
  88. '-webkit-user-select': 'text'
  89. }
  90. },
  91. chatHeader: {
  92. height: '60px',
  93. position: 'relative',
  94. width: '100%',
  95. zIndex: 1,
  96. display: 'flex',
  97. justifyContent: 'space-between',
  98. padding: `${theme.spacing(3)} ${theme.spacing(4)}`,
  99. alignItems: 'center',
  100. boxSizing: 'border-box',
  101. color: theme.palette.text01,
  102. ...withPixelLineHeight(theme.typography.heading6),
  103. '.jitsi-icon': {
  104. cursor: 'pointer'
  105. }
  106. },
  107. chatPanel: {
  108. display: 'flex',
  109. flexDirection: 'column',
  110. // extract header + tabs height
  111. height: 'calc(100% - 110px)'
  112. },
  113. chatPanelNoTabs: {
  114. // extract header height
  115. height: 'calc(100% - 60px)'
  116. },
  117. pollsPanel: {
  118. // extract header + tabs height
  119. height: 'calc(100% - 110px)'
  120. }
  121. };
  122. });
  123. const Chat = ({
  124. _isModal,
  125. _isOpen,
  126. _isPollsEnabled,
  127. _isPollsTabFocused,
  128. _messages,
  129. _nbUnreadMessages,
  130. _nbUnreadPolls,
  131. _onSendMessage,
  132. _onToggleChat,
  133. _onToggleChatTab,
  134. _onTogglePollsTab,
  135. _showNamePrompt,
  136. dispatch,
  137. t
  138. }: IProps) => {
  139. const { classes, cx } = useStyles();
  140. /**
  141. * Sends a text message.
  142. *
  143. * @private
  144. * @param {string} text - The text message to be sent.
  145. * @returns {void}
  146. * @type {Function}
  147. */
  148. const onSendMessage = useCallback((text: string) => {
  149. dispatch(sendMessage(text));
  150. }, []);
  151. /**
  152. * Toggles the chat window.
  153. *
  154. * @returns {Function}
  155. */
  156. const onToggleChat = useCallback(() => {
  157. dispatch(toggleChat());
  158. }, []);
  159. /**
  160. * Click handler for the chat sidenav.
  161. *
  162. * @param {KeyboardEvent} event - Esc key click to close the popup.
  163. * @returns {void}
  164. */
  165. const onEscClick = useCallback((event: React.KeyboardEvent) => {
  166. if (event.key === 'Escape' && _isOpen) {
  167. event.preventDefault();
  168. event.stopPropagation();
  169. onToggleChat();
  170. }
  171. }, [ _isOpen ]);
  172. /**
  173. * Change selected tab.
  174. *
  175. * @param {string} id - Id of the clicked tab.
  176. * @returns {void}
  177. */
  178. const onChangeTab = useCallback((id: string) => {
  179. dispatch(setIsPollsTabFocused(id !== CHAT_TABS.CHAT));
  180. }, []);
  181. /**
  182. * Returns a React Element for showing chat messages and a form to send new
  183. * chat messages.
  184. *
  185. * @private
  186. * @returns {ReactElement}
  187. */
  188. function renderChat() {
  189. return (
  190. <>
  191. {_isPollsEnabled && renderTabs()}
  192. <div
  193. aria-labelledby = { CHAT_TABS.CHAT }
  194. className = { cx(
  195. classes.chatPanel,
  196. !_isPollsEnabled && classes.chatPanelNoTabs,
  197. _isPollsTabFocused && 'hide'
  198. ) }
  199. id = { `${CHAT_TABS.CHAT}-panel` }
  200. role = 'tabpanel'
  201. tabIndex = { 0 }>
  202. <MessageContainer
  203. messages = { _messages } />
  204. <MessageRecipient />
  205. <ChatInput
  206. onSend = { onSendMessage } />
  207. </div>
  208. {_isPollsEnabled && (
  209. <>
  210. <div
  211. aria-labelledby = { CHAT_TABS.POLLS }
  212. className = { cx(classes.pollsPanel, !_isPollsTabFocused && 'hide') }
  213. id = { `${CHAT_TABS.POLLS}-panel` }
  214. role = 'tabpanel'
  215. tabIndex = { 0 }>
  216. <PollsPane />
  217. </div>
  218. <KeyboardAvoider />
  219. </>
  220. )}
  221. </>
  222. );
  223. }
  224. /**
  225. * Returns a React Element showing the Chat and Polls tab.
  226. *
  227. * @private
  228. * @returns {ReactElement}
  229. */
  230. function renderTabs() {
  231. return (
  232. <Tabs
  233. accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
  234. onChange = { onChangeTab }
  235. selected = { _isPollsTabFocused ? CHAT_TABS.POLLS : CHAT_TABS.CHAT }
  236. tabs = { [ {
  237. accessibilityLabel: t('chat.tabs.chat'),
  238. countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
  239. id: CHAT_TABS.CHAT,
  240. controlsId: `${CHAT_TABS.CHAT}-panel`,
  241. label: t('chat.tabs.chat')
  242. }, {
  243. accessibilityLabel: t('chat.tabs.polls'),
  244. countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
  245. id: CHAT_TABS.POLLS,
  246. controlsId: `${CHAT_TABS.POLLS}-panel`,
  247. label: t('chat.tabs.polls')
  248. }
  249. ] } />
  250. );
  251. }
  252. return (
  253. _isOpen ? <div
  254. className = { classes.container }
  255. id = 'sideToolbarContainer'
  256. onKeyDown = { onEscClick } >
  257. <ChatHeader
  258. className = { cx('chat-header', classes.chatHeader) }
  259. isPollsEnabled = { _isPollsEnabled }
  260. onCancel = { onToggleChat } />
  261. {_showNamePrompt
  262. ? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
  263. : renderChat()}
  264. </div> : null
  265. );
  266. };
  267. /**
  268. * Maps (parts of) the redux state to {@link Chat} React {@code Component}
  269. * props.
  270. *
  271. * @param {Object} state - The redux store/state.
  272. * @param {any} _ownProps - Components' own props.
  273. * @private
  274. * @returns {{
  275. * _isModal: boolean,
  276. * _isOpen: boolean,
  277. * _isPollsEnabled: boolean,
  278. * _isPollsTabFocused: boolean,
  279. * _messages: Array<Object>,
  280. * _nbUnreadMessages: number,
  281. * _nbUnreadPolls: number,
  282. * _showNamePrompt: boolean
  283. * }}
  284. */
  285. function _mapStateToProps(state: IReduxState, _ownProps: any) {
  286. const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat'];
  287. const { nbUnreadPolls } = state['features/polls'];
  288. const _localParticipant = getLocalParticipant(state);
  289. return {
  290. _isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
  291. _isOpen: isOpen,
  292. _isPollsEnabled: !arePollsDisabled(state),
  293. _isPollsTabFocused: isPollsTabFocused,
  294. _messages: messages,
  295. _nbUnreadMessages: nbUnreadMessages,
  296. _nbUnreadPolls: nbUnreadPolls,
  297. _showNamePrompt: !_localParticipant?.name
  298. };
  299. }
  300. export default translate(connect(_mapStateToProps)(Chat));