2021-05-29 08:28:47 +08:00
|
|
|
import { defineMessages } from 'react-intl';
|
2024-04-11 04:37:23 +08:00
|
|
|
import { escapeHtml } from '/imports/utils/string-utils';
|
2018-11-01 03:13:19 +08:00
|
|
|
|
2021-02-06 00:29:58 +08:00
|
|
|
const POLL_AVATAR_COLOR = '#3B48A9';
|
2024-02-29 21:35:14 +08:00
|
|
|
const MAX_POLL_RESULT_BARS = 10;
|
|
|
|
const MAX_POLL_RESULT_KEY_LENGTH = 30;
|
2024-02-21 20:16:53 +08:00
|
|
|
const POLL_BAR_CHAR = '\u220E';
|
2021-02-06 00:29:58 +08:00
|
|
|
|
2024-04-11 20:23:51 +08:00
|
|
|
interface PollResultData {
|
|
|
|
id: string;
|
|
|
|
answers: {
|
|
|
|
id: number;
|
|
|
|
key: string;
|
|
|
|
numVotes: number;
|
|
|
|
}[];
|
|
|
|
numRespondents: number;
|
|
|
|
numResponders: number;
|
|
|
|
questionText: string;
|
|
|
|
questionType: string;
|
|
|
|
type: string;
|
|
|
|
whiteboardId: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Intl {
|
|
|
|
formatMessage: (descriptor: { id: string; description: string }) => string;
|
|
|
|
}
|
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
export const pollTypes = {
|
2021-05-27 01:52:55 +08:00
|
|
|
YesNo: 'YN',
|
|
|
|
YesNoAbstention: 'YNA',
|
|
|
|
TrueFalse: 'TF',
|
|
|
|
Letter: 'A-',
|
|
|
|
A2: 'A-2',
|
|
|
|
A3: 'A-3',
|
|
|
|
A4: 'A-4',
|
|
|
|
A5: 'A-5',
|
2021-06-12 00:55:53 +08:00
|
|
|
Custom: 'CUSTOM',
|
2021-05-27 01:52:55 +08:00
|
|
|
Response: 'R-',
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2018-11-01 03:13:19 +08:00
|
|
|
|
2019-05-23 02:00:44 +08:00
|
|
|
const pollAnswerIds = {
|
|
|
|
true: {
|
|
|
|
id: 'app.poll.answer.true',
|
|
|
|
description: 'label for poll answer True',
|
|
|
|
},
|
|
|
|
false: {
|
|
|
|
id: 'app.poll.answer.false',
|
|
|
|
description: 'label for poll answer False',
|
|
|
|
},
|
|
|
|
yes: {
|
|
|
|
id: 'app.poll.answer.yes',
|
|
|
|
description: 'label for poll answer Yes',
|
|
|
|
},
|
|
|
|
no: {
|
|
|
|
id: 'app.poll.answer.no',
|
|
|
|
description: 'label for poll answer No',
|
|
|
|
},
|
2021-01-24 09:22:40 +08:00
|
|
|
abstention: {
|
|
|
|
id: 'app.poll.answer.abstention',
|
|
|
|
description: 'label for poll answer Abstention',
|
|
|
|
},
|
2019-05-23 02:00:44 +08:00
|
|
|
a: {
|
|
|
|
id: 'app.poll.answer.a',
|
|
|
|
description: 'label for poll answer A',
|
|
|
|
},
|
|
|
|
b: {
|
|
|
|
id: 'app.poll.answer.b',
|
|
|
|
description: 'label for poll answer B',
|
|
|
|
},
|
|
|
|
c: {
|
|
|
|
id: 'app.poll.answer.c',
|
|
|
|
description: 'label for poll answer C',
|
|
|
|
},
|
|
|
|
d: {
|
|
|
|
id: 'app.poll.answer.d',
|
|
|
|
description: 'label for poll answer D',
|
|
|
|
},
|
|
|
|
e: {
|
|
|
|
id: 'app.poll.answer.e',
|
|
|
|
description: 'label for poll answer E',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-05-29 08:28:47 +08:00
|
|
|
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',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-04-11 20:23:51 +08:00
|
|
|
const getUsedLabels = (listOfAnswers: PollResultData['answers'], possibleLabels: string[]) => listOfAnswers.map(
|
2023-07-31 22:24:25 +08:00
|
|
|
(answer) => {
|
|
|
|
if (answer.key.length >= 2) {
|
|
|
|
const formattedLabel = answer.key.slice(0, 2).toUpperCase();
|
|
|
|
if (possibleLabels.includes(formattedLabel)) {
|
|
|
|
return formattedLabel;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const getFormattedAnswerValue = (answerText: string) => {
|
2023-07-31 22:24:25 +08:00
|
|
|
// In generatePossibleLabels there is a check to see if the
|
|
|
|
// answer's length is greater than 2
|
|
|
|
const newText = answerText.slice(2).trim();
|
|
|
|
return newText;
|
|
|
|
};
|
|
|
|
|
|
|
|
const generateAlphabetList = () => Array.from(Array(26))
|
2024-04-11 20:23:51 +08:00
|
|
|
.map((_, i) => i + 65).map((x) => String.fromCharCode(x));
|
2023-07-31 22:24:25 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const generatePossibleLabels = (alphabetCharacters: string[]) => {
|
2023-07-31 22:24:25 +08:00
|
|
|
// Remove the Letter from the beginning and the following sign, if any, like so:
|
|
|
|
// "A- the answer is" -> Remove "A-" -> "the answer is"
|
|
|
|
const listOfForbiddenSignsToStart = ['.', ':', '-'];
|
|
|
|
|
|
|
|
const possibleLabels = [];
|
|
|
|
for (let i = 0; i < alphabetCharacters.length; i += 1) {
|
|
|
|
for (let j = 0; j < listOfForbiddenSignsToStart.length; j += 1) {
|
|
|
|
possibleLabels.push(alphabetCharacters[i] + listOfForbiddenSignsToStart[j]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return possibleLabels;
|
|
|
|
};
|
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const truncate = (text: string, length: number) => {
|
2024-02-21 20:16:53 +08:00
|
|
|
let resultText = text;
|
|
|
|
if (resultText.length < length) {
|
|
|
|
const diff = length - resultText.length;
|
|
|
|
const padding = ' '.repeat(diff);
|
|
|
|
resultText += padding;
|
|
|
|
} else if (resultText.length > length) {
|
|
|
|
resultText = `${resultText.substring(0, MAX_POLL_RESULT_KEY_LENGTH - 3)}...`;
|
|
|
|
}
|
|
|
|
return resultText;
|
|
|
|
};
|
|
|
|
|
2024-04-11 20:23:51 +08:00
|
|
|
const getPollResultsText = (isDefaultPoll: boolean, answers: PollResultData['answers'], numRespondents: number, intl: Intl) => {
|
2021-05-20 22:53:52 +08:00
|
|
|
let responded = 0;
|
|
|
|
let resultString = '';
|
|
|
|
let optionsString = '';
|
|
|
|
|
2023-07-31 22:24:25 +08:00
|
|
|
const alphabetCharacters = generateAlphabetList();
|
|
|
|
const possibleLabels = generatePossibleLabels(alphabetCharacters);
|
|
|
|
|
|
|
|
// We need to guarantee that the labels are in the correct order, and that all options have label
|
|
|
|
const pollAnswerMatchLabeledFormat = getUsedLabels(answers, possibleLabels);
|
|
|
|
const isPollAnswerMatchFormat = !isDefaultPoll
|
|
|
|
? pollAnswerMatchLabeledFormat.reduce(
|
|
|
|
(acc, label, index) => acc && !!label && label[0] === alphabetCharacters[index][0], true,
|
|
|
|
)
|
|
|
|
: false;
|
|
|
|
|
2024-02-21 20:16:53 +08:00
|
|
|
let longestKeyLength = answers.reduce(
|
|
|
|
(acc, item) => (item.key.length > acc ? item.key.length : acc), 0,
|
|
|
|
);
|
|
|
|
longestKeyLength = Math.min(longestKeyLength, MAX_POLL_RESULT_KEY_LENGTH);
|
|
|
|
|
2021-05-20 22:53:52 +08:00
|
|
|
answers.map((item) => {
|
|
|
|
responded += item.numVotes;
|
|
|
|
return item;
|
2023-07-31 22:24:25 +08:00
|
|
|
}).forEach((item, index) => {
|
2021-05-20 22:53:52 +08:00
|
|
|
const numResponded = responded === numRespondents ? numRespondents : responded;
|
2024-02-21 20:16:53 +08:00
|
|
|
const pct = Math.round((item.numVotes / (numResponded || 1)) * 100);
|
|
|
|
const pctBars = POLL_BAR_CHAR.repeat((pct * MAX_POLL_RESULT_BARS) / 100);
|
2021-05-20 22:53:52 +08:00
|
|
|
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
|
|
|
|
if (isDefaultPoll) {
|
2024-04-11 20:23:51 +08:00
|
|
|
let translatedKey = pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds]
|
|
|
|
? intl.formatMessage(pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds])
|
2021-06-08 03:21:21 +08:00
|
|
|
: item.key;
|
2024-02-21 20:16:53 +08:00
|
|
|
translatedKey = truncate(translatedKey, longestKeyLength);
|
|
|
|
resultString += `${translatedKey}: ${item.numVotes || 0} ${pctBars}${POLL_BAR_CHAR} ${pctFotmatted}\n`;
|
2021-05-20 22:53:52 +08:00
|
|
|
} else {
|
2023-07-31 22:24:25 +08:00
|
|
|
if (isPollAnswerMatchFormat) {
|
2024-04-11 20:23:51 +08:00
|
|
|
resultString += `${pollAnswerMatchLabeledFormat[index]?.[0]}`;
|
2023-07-31 22:24:25 +08:00
|
|
|
const formattedAnswerValue = getFormattedAnswerValue(item.key);
|
2024-04-11 20:23:51 +08:00
|
|
|
optionsString += `${pollAnswerMatchLabeledFormat[index]?.[0]}: ${formattedAnswerValue}\n`;
|
2023-07-31 22:24:25 +08:00
|
|
|
} else {
|
2024-02-21 20:16:53 +08:00
|
|
|
let { key } = item;
|
|
|
|
key = truncate(key, longestKeyLength);
|
|
|
|
resultString += key;
|
2023-07-31 22:24:25 +08:00
|
|
|
}
|
2024-02-21 20:16:53 +08:00
|
|
|
resultString += `: ${item.numVotes || 0} ${pctBars}${POLL_BAR_CHAR} ${pctFotmatted}\n`;
|
2021-05-20 22:53:52 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return { resultString, optionsString };
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-20 22:53:52 +08:00
|
|
|
|
2024-04-11 20:23:51 +08:00
|
|
|
const getPollResultString = (pollResultData: PollResultData, intl: Intl) => {
|
2024-04-11 04:37:23 +08:00
|
|
|
const formatBoldBlack = (s: string) => s.bold().fontcolor('black');
|
2021-05-29 08:28:47 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const sanitize = (value: string) => escapeHtml(value);
|
2021-05-29 08:28:47 +08:00
|
|
|
|
2021-06-17 06:31:09 +08:00
|
|
|
const { answers, numRespondents, questionType } = pollResultData;
|
2024-02-21 20:16:53 +08:00
|
|
|
const isDefault = isDefaultPoll(questionType);
|
2021-07-03 03:58:33 +08:00
|
|
|
let {
|
|
|
|
resultString,
|
|
|
|
optionsString,
|
2024-02-21 20:16:53 +08:00
|
|
|
} = getPollResultsText(isDefault, answers, numRespondents, intl);
|
2021-05-29 08:28:47 +08:00
|
|
|
resultString = sanitize(resultString);
|
|
|
|
optionsString = sanitize(optionsString);
|
|
|
|
|
|
|
|
let pollText = formatBoldBlack(resultString);
|
2024-02-21 20:16:53 +08:00
|
|
|
if (optionsString !== '') {
|
2021-05-29 08:28:47 +08:00
|
|
|
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;
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-29 08:28:47 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
export const validateInput = (input: string) => {
|
|
|
|
let i = input;
|
|
|
|
while (/^\s/.test(i)) i = i.substring(1);
|
|
|
|
return i;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getSplittedQuestionAndOptions = (questionAndOptions: string[] | string) => {
|
|
|
|
const inputList = Array.isArray(questionAndOptions)
|
|
|
|
? questionAndOptions
|
|
|
|
: questionAndOptions.split('\n').filter((val: string) => val !== '');
|
|
|
|
const splittedQuestion = inputList.length > 0 ? inputList[0] : questionAndOptions;
|
|
|
|
const optList = inputList.slice(1);
|
|
|
|
|
|
|
|
const optionsList = optList.map((val) => {
|
|
|
|
const option = validateInput(val);
|
|
|
|
return { val: option };
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
splittedQuestion,
|
|
|
|
optionsList,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export const removeEmptyLineSpaces = (input: string) => {
|
|
|
|
const filteredInput = input.split('\n').filter((val) => val.trim() !== '');
|
|
|
|
return filteredInput;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const isDefaultPoll = (pollType: string) => pollType !== pollTypes.Response;
|
|
|
|
|
|
|
|
const matchYesNoPoll = (yesValue: string, noValue: string, contentString: string) => {
|
2021-05-27 01:52:55 +08:00
|
|
|
const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`;
|
|
|
|
const ynOptionsRegex = new RegExp(ynPollString, 'gi');
|
2021-08-17 01:25:16 +08:00
|
|
|
const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || [];
|
2021-05-27 01:52:55 +08:00
|
|
|
return ynPoll;
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-27 01:52:55 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const matchYesNoAbstentionPoll = (yesValue:string, noValue:string, abstentionValue:string, contentString:string) => {
|
|
|
|
/* eslint max-len: [off] */
|
2021-05-27 01:52:55 +08:00
|
|
|
const ynaPollString = `(${yesValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${abstentionValue})|(${yesValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${yesValue})|(${noValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${abstentionValue})|(${noValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${yesValue})`;
|
|
|
|
const ynaOptionsRegex = new RegExp(ynaPollString, 'gi');
|
2021-08-17 01:25:16 +08:00
|
|
|
const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || [];
|
2021-05-27 01:52:55 +08:00
|
|
|
return ynaPoll;
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-27 01:52:55 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
const matchTrueFalsePoll = (trueValue:string, falseValue:string, contentString:string) => {
|
2021-05-27 01:52:55 +08:00
|
|
|
const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`;
|
|
|
|
const tgOptionsRegex = new RegExp(tfPollString, 'gi');
|
|
|
|
const tfPoll = contentString.match(tgOptionsRegex) || [];
|
|
|
|
return tfPoll;
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-27 01:52:55 +08:00
|
|
|
|
2024-04-11 04:37:23 +08:00
|
|
|
export const checkPollType = (
|
|
|
|
type: string | null,
|
|
|
|
optList: { val: string }[],
|
|
|
|
yesValue: string,
|
|
|
|
noValue: string,
|
|
|
|
abstentionValue: string,
|
|
|
|
trueValue: string,
|
|
|
|
falseValue: string,
|
2021-07-03 03:58:33 +08:00
|
|
|
) => {
|
2024-04-11 04:37:23 +08:00
|
|
|
/* eslint no-underscore-dangle: "off" */
|
2021-05-27 01:52:55 +08:00
|
|
|
let _type = type;
|
|
|
|
let pollString = '';
|
2024-04-11 04:37:23 +08:00
|
|
|
let defaultMatch: RegExpMatchArray | [] | null = null;
|
2021-05-27 01:52:55 +08:00
|
|
|
let isDefault = null;
|
|
|
|
|
|
|
|
switch (_type) {
|
|
|
|
case pollTypes.Letter:
|
2021-08-17 01:25:16 +08:00
|
|
|
pollString = optList.map((x) => x.val.toUpperCase()).sort().join('');
|
2023-02-02 04:34:25 +08:00
|
|
|
defaultMatch = pollString.match(/^(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi);
|
2021-05-27 01:52:55 +08:00
|
|
|
isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
|
2024-04-11 04:37:23 +08:00
|
|
|
_type = isDefault && Array.isArray(defaultMatch) ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom;
|
2021-05-27 01:52:55 +08:00
|
|
|
break;
|
|
|
|
case pollTypes.TrueFalse:
|
|
|
|
pollString = optList.map((x) => x.val).join('/');
|
|
|
|
defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString);
|
2024-04-11 04:37:23 +08:00
|
|
|
isDefault = defaultMatch.length > 0 && pollString.length === (defaultMatch[0]?.length);
|
2021-05-27 01:52:55 +08:00
|
|
|
if (!isDefault) _type = pollTypes.Custom;
|
|
|
|
break;
|
|
|
|
case pollTypes.YesNoAbstention:
|
|
|
|
pollString = optList.map((x) => x.val).join('/');
|
|
|
|
defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString);
|
2024-04-11 04:37:23 +08:00
|
|
|
isDefault = Array.isArray(defaultMatch) && defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
|
2021-05-27 01:52:55 +08:00
|
|
|
if (!isDefault) {
|
|
|
|
// also try to match only yes/no
|
2021-08-17 01:25:16 +08:00
|
|
|
defaultMatch = matchYesNoPoll(yesValue, noValue, pollString);
|
2024-04-11 04:37:23 +08:00
|
|
|
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
|
2021-05-27 01:52:55 +08:00
|
|
|
_type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return _type;
|
2021-07-03 03:58:33 +08:00
|
|
|
};
|
2021-05-27 01:52:55 +08:00
|
|
|
|
2018-11-01 03:13:19 +08:00
|
|
|
export default {
|
|
|
|
pollTypes,
|
2024-04-11 04:37:23 +08:00
|
|
|
validateInput,
|
|
|
|
getSplittedQuestionAndOptions,
|
|
|
|
removeEmptyLineSpaces,
|
|
|
|
isDefaultPoll,
|
2019-05-23 02:00:44 +08:00
|
|
|
pollAnswerIds,
|
2021-02-06 00:29:58 +08:00
|
|
|
POLL_AVATAR_COLOR,
|
2021-07-03 03:58:33 +08:00
|
|
|
getPollResultString,
|
|
|
|
matchYesNoPoll,
|
|
|
|
matchYesNoAbstentionPoll,
|
|
|
|
matchTrueFalsePoll,
|
|
|
|
checkPollType,
|
2024-02-21 20:16:53 +08:00
|
|
|
POLL_BAR_CHAR,
|
2018-11-01 03:13:19 +08:00
|
|
|
};
|