diff --git a/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx index 95b7854264bfc93650bec400b2c7faca7b4d0373..1a67f173ca51873a0557816de0ecbf2f405e1a25 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx @@ -8,6 +8,9 @@ import ChatPushAlert from './push-alert/component'; import Service from '../service'; import { styles } from '../styles'; +const CHAT_CONFIG = Meteor.settings.public.chat; +const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear; + const propTypes = { pushAlertDisabled: PropTypes.bool.isRequired, activeChats: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -152,7 +155,7 @@ class ChatAlert extends PureComponent { } = this.props; const contentMessage = message .map((content) => { - if (content.text === 'PUBLIC_CHAT_CLEAR') return intl.formatMessage(intlMessages.publicChatClear); + if (content.text === PUBLIC_CHAT_CLEAR) return intl.formatMessage(intlMessages.publicChatClear); /* this code is to remove html tags that come in the server's messages */ const tempDiv = document.createElement('div'); tempDiv.innerHTML = content.text; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx index 3811d4e5853c7a082f1b6cbae334b3e2bfe7d6d8..b2442f25189bd2f8c56e8489a5c5e2b2a5576227 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx @@ -103,8 +103,8 @@ class ChatDropdown extends PureComponent { link.setAttribute('download', `bbb-${meetingName}[public-chat]_${dateString}.txt`); link.setAttribute( 'href', - `data: ${mimeType} ;charset=utf-8, - ${encodeURIComponent(ChatService.exportChat(timeWindowsValues, users, intl))}`, + `data: ${mimeType} ;charset=utf-8,`+ + `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, users, intl))}`, ); link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); }} diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 65c3dcd0f3716c6ed44a7da5e25fbaec283f1f4c..89d94ad4dfd4de0618194770c1a895ee7a83b6a4 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -8,6 +8,7 @@ import { makeCall } from '/imports/ui/services/api'; import _ from 'lodash'; import { meetingIsBreakout } from '/imports/ui/components/app/service'; import { defineMessages } from 'react-intl'; +import PollService from '/imports/ui/components/poll/service'; const CHAT_CONFIG = Meteor.settings.public.chat; const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window; @@ -17,6 +18,7 @@ const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; +const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.chat_clear; const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -35,6 +37,10 @@ const intlMessages = defineMessages({ id: 'app.chat.clearPublicChatMessage', description: 'message of when clear the public chat', }, + pollResult: { + id: 'app.chat.pollResult', + description: 'used in place of user name who published poll to chat', + }, }); const setUserSentMessage = (bool) => { @@ -251,10 +257,21 @@ const exportChat = (timeWindowList, users, intl) => { const hour = date.getHours().toString().padStart(2, 0); const min = date.getMinutes().toString().padStart(2, 0); const hourMin = `[${hour}:${min}]`; - const userName = message.id.startsWith('SYSTEM_MESSAGE') + let userName = message.id.startsWith(SYSTEM_CHAT_TYPE) ? '' : `${users[timeWindow.sender].name}: `; - const messageText = (message.text === 'PUBLIC_CHAT_CLEAR') ? intl.formatMessage(intlMessages.publicChatClear) : message.text; + let messageText = ''; + if (message.text === PUBLIC_CHAT_CLEAR) { + message.text = intl.formatMessage(intlMessages.publicChatClear); + } else if (message.id.includes(CHAT_POLL_RESULTS_MESSAGE)) { + userName = `${intl.formatMessage(intlMessages.pollResult)}:\n`; + const { pollResultData } = timeWindow.extra; + const pollText = htmlDecode(PollService.getPollResultString(pollResultData, intl)); + // remove last \n to avoid empty line + messageText = pollText.slice(0, -1); + } else { + messageText = message.text; + } return `${hourMin} ${userName}${htmlDecode(messageText)}`; }); diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx index 372d9c721b64daa65de29352369b3588d70ccf79..270488b44ba54a2f6acb51cebf592f5d2186756a 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx @@ -12,6 +12,7 @@ import TimeWindowChatItem from './time-window-chat-item/container'; const CHAT_CONFIG = Meteor.settings.public.chat; const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result; +const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear; const propTypes = { scrollPosition: PropTypes.number, @@ -151,7 +152,7 @@ class TimeWindowList extends PureComponent { // this condition exist to the case where the chat has a single message and the chat is cleared // The component List from react-virtualized doesn't have a reference to the list of messages so I need force the update to fix it if ( - (lastTimeWindow?.id === 'SYSTEM_MESSAGE-PUBLIC_CHAT_CLEAR') + (lastTimeWindow?.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`) || (prevSyncing && !syncing) || (syncedPercent !== prevSyncedPercent) || (chatId !== prevChatId) diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx index ff9b39f9279898c9b1989714690e1bb7d3cfdd40..df6afc5fee3fca6f7085d3738de3f0e3444f15ba 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/component.jsx @@ -95,7 +95,7 @@ class TimeWindowChatItem extends PureComponent { text={intlMessages[message.text] ? intl.formatMessage(intlMessages[message.text]) : message.text } time={message.time} isSystemMessage={message.id ? true : false} - systemMessageType={message.text === 'PUBLIC_CHAT_CLEAR' ? 'chatClearMessageText' : 'chatWelcomeMessageText'} + systemMessageType={message.text === CHAT_CLEAR_MESSAGE ? 'chatClearMessageText' : 'chatWelcomeMessageText'} chatAreaId={chatAreaId} handleReadMessage={handleReadMessage} /> @@ -194,7 +194,6 @@ class TimeWindowChatItem extends PureComponent { timestamp, color, intl, - isDefaultPoll, getPollResultString, messages, extra, @@ -231,16 +230,13 @@ class TimeWindowChatItem extends PureComponent { type="poll" className={cx(styles.message, styles.pollWrapper)} key={messages[0].id} - text={messages[0].text} - pollResultData={extra.pollResultData} + text={getPollResultString(extra.pollResultData, intl)} time={messages[0].time} chatAreaId={chatAreaId} lastReadMessageTime={lastReadMessageTime} handleReadMessage={handleReadMessage} scrollArea={scrollArea} color={color} - isDefaultPoll={isDefaultPoll(extra.pollResultData.pollType)} - getPollResultString={getPollResultString} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx index cc18f6de86acd51e1c4106d3864599f05110689f..8e4c63edd8007acff8cba6699da160ded4a65be2 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx @@ -37,7 +37,6 @@ export default function TimeWindowChatItemContainer(props) { read: message.read, messages, extra, - isDefaultPoll: PollService.isDefaultPoll, getPollResultString: PollService.getPollResultString, user, timestamp, diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx index 837881c70ff73ac76ca849b8b5201848ed81c17f..81b1ab313c2ac125c9c80d3975d493ea95f76aa9 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component.jsx @@ -2,9 +2,8 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; import fastdom from 'fastdom'; -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger'; -import PollListItem from './poll-list-item/component'; const propTypes = { text: PropTypes.string.isRequired, @@ -36,17 +35,6 @@ const isElementInViewport = (el) => { ); }; -const intlMessages = defineMessages({ - legendTitle: { - id: 'app.polling.pollingTitle', - description: 'heading for chat poll legend', - }, - pollQuestionTitle: { - id: 'app.polling.pollQuestionTitle', - description: 'title displayed before poll question', - }, -}); - class MessageChatItem extends PureComponent { constructor(props) { super(props); @@ -163,7 +151,6 @@ class MessageChatItem extends PureComponent { render() { const { - intl, text, type, className, @@ -171,31 +158,28 @@ class MessageChatItem extends PureComponent { chatUserMessageItem, systemMessageType, color, - isDefaultPoll, - getPollResultString, - pollResultData, } = this.props; ChatLogger.debug('MessageChatItem::render', this.props); - if (type === 'poll') return ( - <PollListItem - intl={intl} - text={text} - pollResultData={pollResultData} - className={className} - color={color} - isDefaultPoll={isDefaultPoll} - getPollResultString={getPollResultString} - /> - ) - - return ( - <p - className={className} - ref={(ref) => { this.text = ref; }} - dangerouslySetInnerHTML={{ __html: text }} - data-test={isSystemMessage ? systemMessageType : chatUserMessageItem ? 'chatUserMessageText' : ''} - /> - ); + if (type === 'poll') { + return ( + <p + className={className} + style={{ borderLeft: `3px ${color} solid`, whiteSpace: 'pre-wrap' }} + ref={(ref) => { this.text = ref; }} + dangerouslySetInnerHTML={{ __html: text }} + data-test="chatPollMessageText" + /> + ); + } else { + return ( + <p + className={className} + ref={(ref) => { this.text = ref; }} + dangerouslySetInnerHTML={{ __html: text }} + data-test={isSystemMessage ? systemMessageType : chatUserMessageItem ? 'chatUserMessageText' : ''} + /> + ); + } } } diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/component.jsx deleted file mode 100644 index 91856398e6a30de1c307ba028d17d759d51a10f5..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/component.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import cx from 'classnames'; -import { styles } from './styles'; - -const propTypes = { - intl: PropTypes.shape({ - formatMessage: PropTypes.func.isRequired, - }).isRequired, - text: PropTypes.string.isRequired, - pollResultData: PropTypes.object.isRequired, - isDefaultPoll: PropTypes.bool.isRequired, - getPollResultString: PropTypes.func.isRequired, -}; - -const intlMessages = defineMessages({ - legendTitle: { - id: 'app.polling.pollingTitle', - description: 'heading for chat poll legend', - }, - pollQuestionTitle: { - id: 'app.polling.pollQuestionTitle', - description: 'title displayed before poll question', - }, -}); - -function PollListItem(props) { - const { - intl, - pollResultData, - className, - color, - isDefaultPoll, - getPollResultString, - } = props; - - const formatBoldBlack = s => s.bold().fontcolor('black'); - - // Sanitize. See: https://gist.github.com/sagewall/47164de600df05fb0f6f44d48a09c0bd - const sanitize = (value) => { - const div = document.createElement('div'); - div.appendChild(document.createTextNode(value)); - return div.innerHTML; - }; - - const { answers, numRespondents } = pollResultData; - let { resultString, optionsString } = getPollResultString(isDefaultPoll, answers, numRespondents, intl) - resultString = sanitize(resultString); - optionsString = sanitize(optionsString); - - let pollText = formatBoldBlack(resultString); - if (!isDefaultPoll) { - pollText += formatBoldBlack(`<br/><br/>${intl.formatMessage(intlMessages.legendTitle)}<br/>`); - pollText += optionsString; - } - - const pollQuestion = pollResultData.title; - if (pollQuestion.trim() !== '') { - const sanitizedPollQuestion = sanitize(pollQuestion.split('<br#>').join(' ')); - - pollText = `${formatBoldBlack(intl.formatMessage(intlMessages.pollQuestionTitle))}<br/>${sanitizedPollQuestion}<br/><br/>${pollText}`; - } - - return ( - <p - className={cx(className, styles.pollText)} - style={{ borderLeft: `3px ${color} solid`}} - ref={(ref) => { this.text = ref; }} - dangerouslySetInnerHTML={{ __html: pollText }} - data-test="chatPollMessageText" - /> - ); -} - -PollListItem.propTypes = propTypes; - -export default injectIntl(PollListItem); \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/styles.scss deleted file mode 100644 index dbb506d3b74410bc18fdcd76e480e290f4e2f257..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/poll-list-item/styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.pollText { - white-space: pre-wrap; -} \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx index a36b24de63cc097c96b1ef6e3288b2463e207480..db9c29bb66bf2c0e0c588d4eadd14de6a97c87c9 100644 --- a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx +++ b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx @@ -11,6 +11,8 @@ let currentUserData = {}; let messageQueue = []; const CHAT_CONFIG = Meteor.settings.public.chat; +const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; +const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear; const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage; const TIME_BETWEEN_FETCHS = CHAT_CONFIG.timeBetweenFetchs; const EVENT_NAME = 'bbb-group-chat-messages-subscription-has-stoppped'; @@ -116,7 +118,7 @@ const Adapter = () => { const parsedMsg = JSON.parse(msg.data); if (parsedMsg.msg === 'added') { const { fields } = parsedMsg; - if (fields.id === 'SYSTEM_MESSAGE-PUBLIC_CHAT_CLEAR') { + if (fields.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`) { messageQueue = []; dispatch({ type: ACTIONS.REMOVED, diff --git a/bigbluebutton-html5/imports/ui/components/poll/service.js b/bigbluebutton-html5/imports/ui/components/poll/service.js index 654482d29b1433343febd329fb515b7e875cde1b..8b61817579e5d737502a0c2ff91686ffb0579911 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/service.js +++ b/bigbluebutton-html5/imports/ui/components/poll/service.js @@ -2,6 +2,7 @@ import Users from '/imports/api/users'; import Auth from '/imports/ui/services/auth'; import Polls from '/imports/api/polls'; import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer'; +import { defineMessages } from 'react-intl'; const POLL_AVATAR_COLOR = '#3B48A9'; const MAX_POLL_RESULT_BARS = 20; @@ -69,7 +70,18 @@ const pollAnswerIds = { }, }; -const getPollResultString = (isDefaultPoll, answers, numRespondents, intl) => { +const intlMessages = defineMessages({ + legendTitle: { + id: 'app.polling.pollingTitle', + description: 'heading for chat poll legend', + }, + pollQuestionTitle: { + id: 'app.polling.pollQuestionTitle', + description: 'title displayed before poll question', + }, +}); + +const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => { let responded = 0; let resultString = ''; let optionsString = ''; @@ -94,6 +106,42 @@ const getPollResultString = (isDefaultPoll, answers, numRespondents, intl) => { return { resultString, optionsString }; } +const isDefaultPoll = (pollType) => { + return pollType !== pollTypes.Custom && pollType !== pollTypes.Response +} + +const getPollResultString = (pollResultData, intl) => { + const formatBoldBlack = s => s.bold().fontcolor('black'); + + // Sanitize. See: https://gist.github.com/sagewall/47164de600df05fb0f6f44d48a09c0bd + const sanitize = (value) => { + const div = document.createElement('div'); + div.appendChild(document.createTextNode(value)); + return div.innerHTML; + }; + + const { answers, numRespondents, pollType } = pollResultData; + const ÃsDefault = isDefaultPoll(pollType) + let { resultString, optionsString } = getPollResultsText(ÃsDefault, answers, numRespondents, intl) + resultString = sanitize(resultString); + optionsString = sanitize(optionsString); + + let pollText = formatBoldBlack(resultString); + if (!ÃsDefault) { + pollText += formatBoldBlack(`<br/><br/>${intl.formatMessage(intlMessages.legendTitle)}<br/>`); + pollText += optionsString; + } + + const pollQuestion = pollResultData.questionText; + if (pollQuestion.trim() !== '') { + const sanitizedPollQuestion = sanitize(pollQuestion.split('<br#>').join(' ')); + + pollText = `${formatBoldBlack(intl.formatMessage(intlMessages.pollQuestionTitle))}<br/>${sanitizedPollQuestion}<br/><br/>${pollText}`; + } + + return pollText; +} + const matchYesNoPoll = (yesValue, noValue, contentString) => { const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`; const ynOptionsRegex = new RegExp(ynPollString, 'gi'); @@ -160,7 +208,7 @@ export default { currentPoll: () => Polls.findOne({ meetingId: Auth.meetingID }), pollAnswerIds, POLL_AVATAR_COLOR, - isDefaultPoll: (pollType) => { return pollType !== pollTypes.Custom && pollType !== pollTypes.Response}, + isDefaultPoll: isDefaultPoll, getPollResultString: getPollResultString, matchYesNoPoll: matchYesNoPoll, matchYesNoAbstentionPoll: matchYesNoAbstentionPoll, diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 10bc805d1017e1f8c0a129bcd3bb7fce46d61a45..09bc2fa79b2cbf849fb2a44b048563995a2b2a64 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -380,7 +380,7 @@ public: poll: enabled: true max_custom: 5 - allowDragAndDropFile: false + allowDragAndDropFile: true maxTypedAnswerLength: 45 captions: enabled: true