refactor: redo how poll result chat message is done +
Fix prolems with ":" char in answers. Respect poll question formatting (lineBreaks/spaces) in other fields of UI
This commit is contained in:
parent
cb4697c932
commit
3a9e9c1358
@ -24,6 +24,7 @@ export default function addSystemMsg(meetingId, chatId, msg) {
|
||||
timestamp: Number,
|
||||
sender: Object,
|
||||
message: String,
|
||||
extra: Object,
|
||||
correlationId: Match.Maybe(String),
|
||||
});
|
||||
const msgDocument = {
|
||||
|
@ -11,9 +11,6 @@ export default function sendPollChatMsg({ body }, meetingId) {
|
||||
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
|
||||
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const MAX_POLL_RESULT_BARS = 20;
|
||||
|
||||
const { answers, numRespondents } = poll;
|
||||
|
||||
const pollData = Polls.findOne({ meetingId });
|
||||
|
||||
@ -21,37 +18,14 @@ export default function sendPollChatMsg({ body }, meetingId) {
|
||||
Logger.error(`Attempted to send chat message of inexisting poll for meetingId: ${meetingId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const caseInsensitiveReducer = (acc, item) => {
|
||||
const index = acc.findIndex(ans => ans.key.toLowerCase() === item.key.toLowerCase());
|
||||
if(index !== -1) {
|
||||
if(acc[index].numVotes >= item.numVotes) acc[index].numVotes += item.numVotes;
|
||||
else {
|
||||
const tempVotes = acc[index].numVotes;
|
||||
acc[index] = item;
|
||||
acc[index].numVotes += tempVotes;
|
||||
}
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
|
||||
const pollResultData = poll;
|
||||
pollResultData.pollType = pollData.pollType;
|
||||
const extra = {
|
||||
type: 'poll',
|
||||
pollResultData,
|
||||
};
|
||||
|
||||
let responded = 0;
|
||||
let resultString = `bbb-published-poll-\n${pollData.question.split('<br/>').join('<br#>').split('\n').join('<br#>')}\n`;
|
||||
|
||||
answers.map((item) => {
|
||||
responded += item.numVotes;
|
||||
return item;
|
||||
}).reduce(caseInsensitiveReducer, []).map((item) => {
|
||||
item.key = item.key.split('<br/>').join('<br#>');
|
||||
const numResponded = responded === numRespondents ? numRespondents : responded;
|
||||
const pct = Math.round(item.numVotes / numResponded * 100);
|
||||
const pctBars = "|".repeat(pct * MAX_POLL_RESULT_BARS / 100);
|
||||
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
|
||||
resultString += `${item.key}: ${item.numVotes || 0} |${pctBars} ${pctFotmatted}\n`;
|
||||
});
|
||||
|
||||
const payload = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${CHAT_POLL_RESULTS_MESSAGE}`,
|
||||
timestamp: Date.now(),
|
||||
@ -60,7 +34,8 @@ export default function sendPollChatMsg({ body }, meetingId) {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: resultString,
|
||||
message: '',
|
||||
extra,
|
||||
};
|
||||
|
||||
removePoll(meetingId, pollData.id);
|
||||
|
@ -17,6 +17,8 @@ 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 CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const ScrollCollection = new Mongo.Collection(null);
|
||||
@ -28,8 +30,6 @@ export const UserSentMessageCollection = new Mongo.Collection(null);
|
||||
// session for closed chat list
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
const POLL_MESSAGE_PREFIX = 'bbb-published-poll-<br/>';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
publicChatClear: {
|
||||
id: 'app.chat.clearPublicChatMessage',
|
||||
@ -94,8 +94,8 @@ const reduceGroupMessages = (previous, current) => {
|
||||
// between the two messages exceeds window and then group current
|
||||
// message with the last one
|
||||
const timeOfLastMessage = lastMessage.content[lastMessage.content.length - 1].time;
|
||||
const isOrWasPoll = currentMessage.message.includes(POLL_MESSAGE_PREFIX)
|
||||
|| lastMessage.message.includes(POLL_MESSAGE_PREFIX);
|
||||
const isOrWasPoll = currentMessage.id.includes(CHAT_POLL_RESULTS_MESSAGE)
|
||||
|| lastMessage.id.includes(CHAT_POLL_RESULTS_MESSAGE);
|
||||
const groupingWindow = isOrWasPoll ? 0 : GROUPING_MESSAGES_WINDOW;
|
||||
|
||||
if (lastMessage.sender.id === currentMessage.sender.id
|
||||
|
@ -12,6 +12,7 @@ import { styles } from './styles';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.shape({
|
||||
@ -78,7 +79,7 @@ class TimeWindowChatItem extends PureComponent {
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
if (messages && messages[0].text.includes('bbb-published-poll-<br/>')) {
|
||||
if (messages && messages[0].id.includes(CHAT_POLL_RESULTS_MESSAGE)) {
|
||||
return this.renderPollItem();
|
||||
}
|
||||
|
||||
@ -194,8 +195,9 @@ class TimeWindowChatItem extends PureComponent {
|
||||
color,
|
||||
intl,
|
||||
isDefaultPoll,
|
||||
extractPollQuestion,
|
||||
getPollResultString,
|
||||
messages,
|
||||
extra,
|
||||
scrollArea,
|
||||
chatAreaId,
|
||||
lastReadMessageTime,
|
||||
@ -230,14 +232,15 @@ class TimeWindowChatItem extends PureComponent {
|
||||
className={cx(styles.message, styles.pollWrapper)}
|
||||
key={messages[0].id}
|
||||
text={messages[0].text}
|
||||
pollResultData={extra.pollResultData}
|
||||
time={messages[0].time}
|
||||
chatAreaId={chatAreaId}
|
||||
lastReadMessageTime={lastReadMessageTime}
|
||||
handleReadMessage={handleReadMessage}
|
||||
scrollArea={scrollArea}
|
||||
color={color}
|
||||
isDefaultPoll={isDefaultPoll(messages[0].text.replace('bbb-published-poll-<br/>', ''))}
|
||||
extractPollQuestion={extractPollQuestion}
|
||||
isDefaultPoll={isDefaultPoll(extra.pollResultData.pollType)}
|
||||
getPollResultString={getPollResultString}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,33 +3,13 @@ import TimeWindowChatItem from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import ChatService from '../../service';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const extractPollQuestion = (pollText) => {
|
||||
if (!pollText) return {};
|
||||
|
||||
const pollQuestion = pollText.split('<br/>')[0];
|
||||
pollText = pollText.replace(`${pollQuestion}<br/>`,'');
|
||||
|
||||
return { pollQuestion, pollText };
|
||||
};
|
||||
|
||||
const isDefaultPoll = (pollText) => {
|
||||
const { pollQuestion, pollText: newPollText} = extractPollQuestion(pollText);
|
||||
|
||||
const pollValue = newPollText.replace(/<br\/>|[ :|%\n\d+]/g, '');
|
||||
switch (pollValue) {
|
||||
case 'A': case 'AB': case 'ABC': case 'ABCD':
|
||||
case 'ABCDE': case 'YesNo': case 'TrueFalse':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export default function TimeWindowChatItemContainer(props) {
|
||||
ChatLogger.debug('TimeWindowChatItemContainer::render', { ...props });
|
||||
const { message, messageId } = props;
|
||||
@ -40,6 +20,7 @@ export default function TimeWindowChatItemContainer(props) {
|
||||
key,
|
||||
timestamp,
|
||||
content,
|
||||
extra,
|
||||
} = message;
|
||||
const messages = content;
|
||||
const user = users[Auth.meetingID][sender];
|
||||
@ -55,8 +36,9 @@ export default function TimeWindowChatItemContainer(props) {
|
||||
name: user?.name,
|
||||
read: message.read,
|
||||
messages,
|
||||
isDefaultPoll,
|
||||
extractPollQuestion,
|
||||
extra,
|
||||
isDefaultPoll: PollService.isDefaultPoll,
|
||||
getPollResultString: PollService.getPollResultString,
|
||||
user,
|
||||
timestamp,
|
||||
systemMessage: messageId.startsWith(SYSTEM_CHAT_TYPE) || !sender,
|
||||
|
@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
import fastdom from 'fastdom';
|
||||
import { defineMessages, 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,
|
||||
@ -53,8 +54,6 @@ class MessageChatItem extends PureComponent {
|
||||
this.ticking = false;
|
||||
|
||||
this.handleMessageInViewport = _.debounce(this.handleMessageInViewport.bind(this), 50);
|
||||
|
||||
this.renderPollListItem = this.renderPollListItem.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -162,91 +161,32 @@ class MessageChatItem extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
renderPollListItem() {
|
||||
const {
|
||||
intl,
|
||||
text,
|
||||
className,
|
||||
color,
|
||||
isDefaultPoll,
|
||||
extractPollQuestion,
|
||||
} = this.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;
|
||||
};
|
||||
|
||||
let _text = text.replace('bbb-published-poll-<br/>', '');
|
||||
|
||||
const { pollQuestion, pollText: newPollText } = extractPollQuestion(_text);
|
||||
_text = newPollText;
|
||||
|
||||
if (!isDefaultPoll) {
|
||||
const entries = _text.split('<br/>');
|
||||
const options = [];
|
||||
_text = _text.split('<br#>').join('<br/>');
|
||||
|
||||
entries.map((e) => {
|
||||
e = e.split('<br#>').join('<br/>');
|
||||
const sanitizedEntry = sanitize(e);
|
||||
_text = _text.replace(e, sanitizedEntry);
|
||||
e = sanitizedEntry;
|
||||
|
||||
options.push([e.slice(0, e.indexOf(':'))]);
|
||||
return e;
|
||||
});
|
||||
options.map((o, idx) => {
|
||||
if (o[0] !== '') {
|
||||
_text = formatBoldBlack(_text.replace(o, idx + 1));
|
||||
}
|
||||
return _text;
|
||||
});
|
||||
_text += formatBoldBlack(`<br/><br/>${intl.formatMessage(intlMessages.legendTitle)}`);
|
||||
options.map((o, idx) => {
|
||||
if (o[0] !== '') {
|
||||
_text += `<br/>${idx + 1}: ${o}`;
|
||||
}
|
||||
return _text;
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefaultPoll) {
|
||||
_text = formatBoldBlack(_text);
|
||||
}
|
||||
|
||||
if (pollQuestion.trim() !== '') {
|
||||
const sanitizedPollQuestion = sanitize(pollQuestion.split('<br#>').join(' '));
|
||||
|
||||
_text = `${formatBoldBlack(intl.formatMessage(intlMessages.pollQuestionTitle))}<br/>${sanitizedPollQuestion}<br/><br/>${_text}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
style={{ borderLeft: `3px ${color} solid` }}
|
||||
ref={(ref) => { this.text = ref; }}
|
||||
dangerouslySetInnerHTML={{ __html: _text }}
|
||||
data-test="chatPollMessageText"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
text,
|
||||
type,
|
||||
className,
|
||||
isSystemMessage,
|
||||
chatUserMessageItem,
|
||||
systemMessageType,
|
||||
color,
|
||||
isDefaultPoll,
|
||||
getPollResultString,
|
||||
pollResultData,
|
||||
} = this.props;
|
||||
ChatLogger.debug('MessageChatItem::render', this.props);
|
||||
if (type === 'poll') return this.renderPollListItem();
|
||||
if (type === 'poll') return (
|
||||
<PollListItem
|
||||
intl={intl}
|
||||
text={text}
|
||||
pollResultData={pollResultData}
|
||||
className={className}
|
||||
color={color}
|
||||
isDefaultPoll={isDefaultPoll}
|
||||
getPollResultString={getPollResultString}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<p
|
||||
|
@ -0,0 +1,78 @@
|
||||
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)
|
||||
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);
|
@ -0,0 +1,3 @@
|
||||
.pollText {
|
||||
white-space: pre-wrap;
|
||||
}
|
@ -167,7 +167,8 @@
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@keyframes ellipsis {
|
||||
|
@ -1,8 +1,10 @@
|
||||
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';
|
||||
|
||||
const POLL_AVATAR_COLOR = '#3B48A9';
|
||||
const MAX_POLL_RESULT_BARS = 20;
|
||||
|
||||
// 'YN' = Yes,No
|
||||
// 'YNA' = Yes,No,Abstention
|
||||
@ -56,6 +58,30 @@ const pollAnswerIds = {
|
||||
},
|
||||
};
|
||||
|
||||
const getPollResultString = (isDefaultPoll, answers, numRespondents) => {
|
||||
let responded = 0;
|
||||
let resultString = '';
|
||||
let optionsString = '';
|
||||
|
||||
answers.map((item) => {
|
||||
responded += item.numVotes;
|
||||
return item;
|
||||
}).reduce(caseInsensitiveReducer, []).map((item) => {
|
||||
const numResponded = responded === numRespondents ? numRespondents : responded;
|
||||
const pct = Math.round(item.numVotes / numResponded * 100);
|
||||
const pctBars = "|".repeat(pct * MAX_POLL_RESULT_BARS / 100);
|
||||
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
|
||||
if (isDefaultPoll) {
|
||||
resultString += `${item.key}: ${item.numVotes || 0} |${pctBars} ${pctFotmatted}\n`;
|
||||
} else {
|
||||
resultString += `${item.id+1}: ${item.numVotes || 0} |${pctBars} ${pctFotmatted}\n`;
|
||||
optionsString += `${item.id+1}: ${item.key}\n`;
|
||||
}
|
||||
});
|
||||
|
||||
return { resultString, optionsString };
|
||||
}
|
||||
|
||||
export default {
|
||||
amIPresenter: () => Users.findOne(
|
||||
{ userId: Auth.userID },
|
||||
@ -65,4 +91,6 @@ export default {
|
||||
currentPoll: () => Polls.findOne({ meetingId: Auth.meetingID }),
|
||||
pollAnswerIds,
|
||||
POLL_AVATAR_COLOR,
|
||||
isDefaultPoll: (pollType) => { return pollType !== 'custom' && pollType !== 'R-'},
|
||||
getPollResultString: getPollResultString,
|
||||
};
|
||||
|
@ -142,6 +142,7 @@
|
||||
.qText {
|
||||
color: var(--color-text);
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-size: var(--font-size-large);
|
||||
max-width: var(--poll-width);
|
||||
padding-right: var(--sm-padding-x);
|
||||
|
Loading…
Reference in New Issue
Block a user