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.4KB

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