diff --git a/debian/jitsi-meet-prosody.postinst b/debian/jitsi-meet-prosody.postinst index a2ff18e398..a06edeccc1 100644 --- a/debian/jitsi-meet-prosody.postinst +++ b/debian/jitsi-meet-prosody.postinst @@ -154,6 +154,16 @@ case "$1" in PROSODY_CONFIG_PRESENT="false" fi + # Start using the polls component + if ! grep -q "Component \"polls.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG ;then + echo -e "\nComponent \"polls.$JVB_HOSTNAME\" \"polls_component\"" >> $PROSODY_HOST_CONFIG + PROSODY_CONFIG_PRESENT="false" + fi + if ! grep -q -- '--"polls";' $PROSODY_HOST_CONFIG ;then + sed -i "s/\"polls\";/--\"polls\";/g" $PROSODY_HOST_CONFIG + PROSODY_CONFIG_PRESENT="false" + fi + # Old versions of jitsi-meet-prosody come with the extra plugin path commented out (https://github.com/jitsi/jitsi-meet/commit/e11d4d3101e5228bf956a69a9e8da73d0aee7949) # Make sure it is uncommented, as it contains required modules. if grep -q -- '--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }' $PROSODY_HOST_CONFIG ;then diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example index a98abdfcb7..bfd5e6a0ff 100644 --- a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example +++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example @@ -83,7 +83,6 @@ Component "conference.jitmeet.example.com" "muc" "muc_hide_all"; "muc_meeting_id"; "muc_domain_mapper"; - "polls"; --"token_verification"; "muc_rate_limit"; "muc_password_whitelist"; @@ -159,9 +158,10 @@ Component "lobby.jitmeet.example.com" "muc" modules_enabled = { "muc_hide_all"; "muc_rate_limit"; - "polls"; } Component "metadata.jitmeet.example.com" "room_metadata_component" muc_component = "conference.jitmeet.example.com" breakout_rooms_component = "breakout.jitmeet.example.com" + +Component "polls.jitmeet.example.com" "polls_component" diff --git a/lang/main.json b/lang/main.json index 62ba251bd6..03e64224d7 100644 --- a/lang/main.json +++ b/lang/main.json @@ -964,6 +964,9 @@ "by": "By {{ name }}", "closeButton": "Close poll", "create": { + "accessibilityLabel": { + "send": "Send poll" + }, "addOption": "Add option", "answerPlaceholder": "Option {{index}}", "cancel": "Cancel", @@ -972,8 +975,7 @@ "pollQuestion": "Poll Question", "questionPlaceholder": "Ask a question", "removeOption": "Remove option", - "save": "Save", - "send": "Send" + "save": "Save" }, "errors": { "notUniqueOption": "Options must be unique" diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index 2513c77318..99c8ce1bde 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -107,6 +107,7 @@ export interface IJitsiConference { getParticipantById: Function; getParticipantCount: Function; getParticipants: Function; + getPolls: Function; getRole: Function; getShortTermCredentials: Function; getSpeakerStats: () => ISpeakerStats; diff --git a/react/features/base/ui/components/web/Checkbox.tsx b/react/features/base/ui/components/web/Checkbox.tsx index 857d23c49e..6ec1a55835 100644 --- a/react/features/base/ui/components/web/Checkbox.tsx +++ b/react/features/base/ui/components/web/Checkbox.tsx @@ -22,6 +22,11 @@ interface ICheckboxProps { */ disabled?: boolean; + /** + * The id of the input. + */ + id?: string; + /** * The label of the input. */ @@ -147,6 +152,7 @@ const Checkbox = ({ checked, className, disabled, + id, label, name, onChange @@ -160,6 +166,7 @@ const Checkbox = ({ diff --git a/react/features/conference/functions.any.ts b/react/features/conference/functions.any.ts index 6b766d834f..a918bdcba0 100644 --- a/react/features/conference/functions.any.ts +++ b/react/features/conference/functions.any.ts @@ -30,5 +30,11 @@ export function shouldDisplayNotifications(stateful: IStateful) { export function arePollsDisabled(stateful: IStateful) { const state = toState(stateful); + const { conference } = state['features/base/conference']; + + if (!conference?.getPolls()?.isSupported()) { + return true; + } + return state['features/base/config']?.disablePolls || iAmVisitor(state); } diff --git a/react/features/polls-history/middleware.ts b/react/features/polls-history/middleware.ts index 05f673f919..367d51babb 100644 --- a/react/features/polls-history/middleware.ts +++ b/react/features/polls-history/middleware.ts @@ -21,7 +21,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { for (const key in pollsHistory) { if (pollsHistory.hasOwnProperty(key) && pollsHistory[key].saved) { - dispatch(savePoll(key, pollsHistory[key])); + dispatch(savePoll(pollsHistory[key])); } } break; diff --git a/react/features/polls-history/reducer.ts b/react/features/polls-history/reducer.ts index 94e0af6629..960e916558 100644 --- a/react/features/polls-history/reducer.ts +++ b/react/features/polls-history/reducer.ts @@ -1,6 +1,6 @@ import PersistenceRegistry from '../base/redux/PersistenceRegistry'; import ReducerRegistry from '../base/redux/ReducerRegistry'; -import { IPoll } from '../polls/types'; +import { IPollData } from '../polls/types'; import { REMOVE_POLL_FROM_HISTORY, SAVE_POLL_IN_HISTORY } from './actionTypes'; @@ -11,7 +11,7 @@ const INITIAL_STATE = { export interface IPollsHistoryState { polls: { [meetingId: string]: { - [pollId: string]: IPoll; + [pollId: string]: IPollData; }; }; } diff --git a/react/features/polls/actionTypes.ts b/react/features/polls/actionTypes.ts index d3b5a24708..349976a312 100644 --- a/react/features/polls/actionTypes.ts +++ b/react/features/polls/actionTypes.ts @@ -35,7 +35,6 @@ export const EDIT_POLL = 'EDIT_POLL'; * { * type: RECEIVE_POLL, * poll: Poll, - * pollId: string, * notify: boolean * } * @@ -47,8 +46,7 @@ export const RECEIVE_POLL = 'RECEIVE_POLL'; * * { * type: RECEIVE_ANSWER, - * answer: Answer, - * pollId: string, + * answer: IIncomingAnswerData * } */ export const RECEIVE_ANSWER = 'RECEIVE_ANSWER'; @@ -89,9 +87,7 @@ export const RESET_NB_UNREAD_POLLS = 'RESET_NB_UNREAD_POLLS'; * * { * type: SAVE_POLL, - * poll: Poll, - * pollId: string, - * saved: boolean + * poll: IPollData * } */ export const SAVE_POLL = 'SAVE_POLL'; diff --git a/react/features/polls/actions.ts b/react/features/polls/actions.ts index 24987734d3..0f83f94774 100644 --- a/react/features/polls/actions.ts +++ b/react/features/polls/actions.ts @@ -9,7 +9,7 @@ import { RESET_NB_UNREAD_POLLS, SAVE_POLL } from './actionTypes'; -import { IAnswer, IPoll } from './types'; +import { IIncomingAnswerData, IPoll, IPollData } from './types'; /** * Action to signal that existing polls needs to be cleared from state. @@ -47,7 +47,6 @@ export const setVoteChanging = (pollId: string, value: boolean) => { /** * Action to signal that a new poll was received. * - * @param {string} pollId - The id of the incoming poll. * @param {IPoll} poll - The incoming Poll object. * @param {boolean} notify - Whether to send or not a notification. * @returns {{ @@ -57,10 +56,9 @@ export const setVoteChanging = (pollId: string, value: boolean) => { * notify: boolean * }} */ -export const receivePoll = (pollId: string, poll: IPoll, notify: boolean) => { +export const receivePoll = (poll: IPoll, notify: boolean) => { return { type: RECEIVE_POLL, - pollId, poll, notify }; @@ -69,18 +67,15 @@ export const receivePoll = (pollId: string, poll: IPoll, notify: boolean) => { /** * Action to signal that a new answer was received. * - * @param {string} pollId - The id of the incoming poll. - * @param {IAnswer} answer - The incoming Answer object. + * @param {IIncomingAnswerData} answer - The incoming Answer object. * @returns {{ * type: RECEIVE_ANSWER, - * pollId: string, - * answer: IAnswer + * answer: IIncomingAnswerData * }} */ -export const receiveAnswer = (pollId: string, answer: IAnswer) => { +export const receiveAnswer = (answer: IIncomingAnswerData) => { return { type: RECEIVE_ANSWER, - pollId, answer }; }; @@ -120,19 +115,15 @@ export function resetNbUnreadPollsMessages() { /** * Action to signal saving a poll. * - * @param {string} pollId - The id of the poll that gets to be saved. - * @param {IPoll} poll - The Poll object that gets to be saved. + * @param {IPollData} poll - The Poll object that gets to be saved. * @returns {{ * type: SAVE_POLL, - * meetingId: string, - * pollId: string, - * poll: IPoll + * poll: IPollData * }} */ -export function savePoll(pollId: string, poll: IPoll) { +export function savePoll(poll: IPollData) { return { type: SAVE_POLL, - pollId, poll }; } @@ -159,18 +150,15 @@ export function editPoll(pollId: string, editing: boolean) { /** * Action to signal that existing polls needs to be removed. * - * @param {string} pollId - The id of the poll that gets to be removed. * @param {IPoll} poll - The incoming Poll object. * @returns {{ * type: REMOVE_POLL, - * pollId: string, * poll: IPoll * }} */ -export const removePoll = (pollId: string, poll: IPoll) => { +export const removePoll = (poll: IPoll) => { return { type: REMOVE_POLL, - pollId, poll }; }; diff --git a/react/features/polls/components/AbstractPollAnswer.tsx b/react/features/polls/components/AbstractPollAnswer.tsx index a5ec77033c..33429c0fef 100644 --- a/react/features/polls/components/AbstractPollAnswer.tsx +++ b/react/features/polls/components/AbstractPollAnswer.tsx @@ -8,9 +8,8 @@ import { IReduxState } from '../../app/types'; import { getParticipantDisplayName } from '../../base/participants/functions'; import { useBoundSelector } from '../../base/util/hooks'; import { registerVote, removePoll, setVoteChanging } from '../actions'; -import { COMMAND_ANSWER_POLL, COMMAND_NEW_POLL } from '../constants'; import { getPoll } from '../functions'; -import { IPoll } from '../types'; +import { IPollData } from '../types'; /** * The type of the React {@code Component} props of inheriting component. @@ -27,8 +26,7 @@ type InputProps = { export type AbstractProps = { checkBoxStates: boolean[]; creatorName: string; - poll: IPoll; - pollId: string; + poll: IPollData; sendPoll: () => void; setCheckbox: Function; setCreateMode: (mode: boolean) => void; @@ -51,7 +49,7 @@ const AbstractPollAnswer = (Component: ComponentType) => (props: const { conference } = useSelector((state: IReduxState) => state['features/base/conference']); - const poll: IPoll = useSelector(getPoll(pollId)); + const poll: IPollData = useSelector(getPoll(pollId)); const { answers, lastVote, question, senderId } = poll; @@ -76,11 +74,7 @@ const AbstractPollAnswer = (Component: ComponentType) => (props: const dispatch = useDispatch(); const submitAnswer = useCallback(() => { - conference?.sendMessage({ - type: COMMAND_ANSWER_POLL, - pollId, - answers: checkBoxStates - }); + conference?.getPolls().answerPoll(pollId, checkBoxStates); sendAnalytics(createPollEvent('vote.sent')); dispatch(registerVote(pollId, checkBoxStates)); @@ -89,14 +83,9 @@ const AbstractPollAnswer = (Component: ComponentType) => (props: }, [ pollId, checkBoxStates, conference ]); const sendPoll = useCallback(() => { - conference?.sendMessage({ - type: COMMAND_NEW_POLL, - pollId, - question, - answers: answers.map(answer => answer.name) - }); + conference?.getPolls().createPoll(pollId, question, answers); - dispatch(removePoll(pollId, poll)); + dispatch(removePoll(poll)); }, [ conference, question, answers ]); const skipAnswer = useCallback(() => { @@ -114,7 +103,6 @@ const AbstractPollAnswer = (Component: ComponentType) => (props: checkBoxStates = { checkBoxStates } creatorName = { participantName } poll = { poll } - pollId = { pollId } sendPoll = { sendPoll } setCheckbox = { setCheckbox } setCreateMode = { setCreateMode } diff --git a/react/features/polls/components/AbstractPollCreate.tsx b/react/features/polls/components/AbstractPollCreate.tsx index 14f6e32415..041dfbb421 100644 --- a/react/features/polls/components/AbstractPollCreate.tsx +++ b/react/features/polls/components/AbstractPollCreate.tsx @@ -10,7 +10,7 @@ import { IReduxState } from '../../app/types'; import { getLocalParticipant } from '../../base/participants/functions'; import { savePoll } from '../actions'; import { hasIdenticalAnswers } from '../functions'; -import { IAnswerData, IPoll } from '../types'; +import { IAnswerData, IPollData } from '../types'; /** * The type of the React {@code Component} props of inheriting component. @@ -26,7 +26,7 @@ type InputProps = { export type AbstractProps = InputProps & { addAnswer: (index?: number) => void; answers: Array; - editingPoll: IPoll | undefined; + editingPoll: IPollData | undefined; editingPollId: string | undefined; isSubmitDisabled: boolean; onSubmit: (event?: FormEvent) => void; @@ -52,7 +52,7 @@ const AbstractPollCreate = (Component: ComponentType) => (props: const pollState = useSelector((state: IReduxState) => state['features/polls'].polls); - const editingPoll: [ string, IPoll ] | null = useMemo(() => { + const editingPoll: [ string, IPollData ] | null = useMemo(() => { if (!pollState) { return null; } @@ -71,12 +71,10 @@ const AbstractPollCreate = (Component: ComponentType) => (props: ? editingPoll[1].answers : [ { - name: '', - voters: [] + name: '' }, { - name: '', - voters: [] + name: '' } ]; }, [ editingPoll ]); @@ -104,8 +102,7 @@ const AbstractPollCreate = (Component: ComponentType) => (props: sendAnalytics(createPollEvent('option.added')); newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, { - name: '', - voters: [] + name: '' }); setAnswers(newAnswers); }, [ answers ]); @@ -140,7 +137,7 @@ const AbstractPollCreate = (Component: ComponentType) => (props: return; } - const poll = { + dispatch(savePoll({ changingVote: false, senderId: localParticipant?.id, showResults: false, @@ -148,14 +145,9 @@ const AbstractPollCreate = (Component: ComponentType) => (props: question, answers: filteredAnswers, saved: true, - editing: false - }; - - if (editingPoll) { - dispatch(savePoll(editingPoll[0], poll)); - } else { - dispatch(savePoll(pollId, poll)); - } + editing: false, + pollId: editingPoll ? editingPoll[0] : pollId + })); sendAnalytics(createPollEvent('created')); diff --git a/react/features/polls/components/AbstractPollResults.tsx b/react/features/polls/components/AbstractPollResults.tsx index d9b377db0f..0355aeb2c3 100644 --- a/react/features/polls/components/AbstractPollResults.tsx +++ b/react/features/polls/components/AbstractPollResults.tsx @@ -10,7 +10,7 @@ import { getParticipantById, getParticipantDisplayName } from '../../base/partic import { useBoundSelector } from '../../base/util/hooks'; import { setVoteChanging } from '../actions'; import { getPoll } from '../functions'; -import { IPoll } from '../types'; +import { IAnswerData, IPollData, IVoterData } from '../types'; /** * The type of the React {@code Component} props of inheriting component. @@ -23,11 +23,9 @@ type InputProps = { pollId: string; }; -export type AnswerInfo = { - name: string; +export type AnswerInfo = IAnswerData & { percentage: number; voterCount: number; - voters?: Array<{ id: string; name: string; } | undefined>; }; /** @@ -38,6 +36,7 @@ export type AbstractProps = { changeVote: (e?: React.MouseEvent | GestureResponderEvent) => void; creatorName: string; haveVoted: boolean; + pollId: string; question: string; showDetails: boolean; t: Function; @@ -54,8 +53,8 @@ export type AbstractProps = { const AbstractPollResults = (Component: ComponentType) => (props: InputProps) => { const { pollId } = props; - const poll: IPoll = useSelector(getPoll(pollId)); - const participant = useBoundSelector(getParticipantById, poll.senderId); + const poll: IPollData = useSelector(getPoll(pollId)); + const creatorName = useBoundSelector(getParticipantDisplayName, poll.senderId); const reduxState = useSelector((state: IReduxState) => state); const [ showDetails, setShowDetails ] = useState(false); @@ -69,33 +68,27 @@ const AbstractPollResults = (Component: ComponentType) => (props: // Getting every voters ID that participates to the poll for (const answer of poll.answers) { - // checking if the voters is an array for supporting old structure model - const voters: string[] = answer.voters.length ? answer.voters : Object.keys(answer.voters); - - voters.forEach((voter: string) => allVoters.add(voter)); + answer.voters?.forEach(k => allVoters.add(k.id)); } return poll.answers.map(answer => { - const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0; + const nrOfVotersPerAnswer = answer.voters?.length || 0; const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0; - let voters; - - if (showDetails && answer.voters) { - const answerVoters = answer.voters?.length ? [ ...answer.voters ] : Object.keys({ ...answer.voters }); - - voters = answerVoters.map(id => { - return { - id, - name: getParticipantDisplayName(reduxState, id) - }; + const voters = answer.voters?.reduce((acc, v) => { + acc.push({ + id: v.id, + name: getParticipantById(reduxState, v.id) + ? getParticipantDisplayName(reduxState, v.id) : v.name }); - } + + return acc; + }, [] as Array); return { name: answer.name, percentage, - voters, + voters: voters, voterCount: nrOfVotersPerAnswer }; }); @@ -113,8 +106,9 @@ const AbstractPollResults = (Component: ComponentType) => (props: { const { checkBoxStates, poll, - pollId, sendPoll, setCheckbox, setCreateMode, @@ -46,7 +45,7 @@ const PollAnswer = (props: AbstractProps) => { { pollSaved && dispatch(removePoll(pollId, poll)) } + onPress = { () => dispatch(removePoll(poll)) } src = { IconCloseLarge } /> } @@ -79,7 +78,7 @@ const PollAnswer = (props: AbstractProps) => { labelKey = 'polls.answer.edit' onClick = { () => { setCreateMode(true); - dispatch(editPoll(pollId, true)); + dispatch(editPoll(poll.pollId, true)); } } style = { pollsStyles.pollCreateButton } type = { SECONDARY } /> diff --git a/react/features/polls/components/native/PollCreate.tsx b/react/features/polls/components/native/PollCreate.tsx index a2bb254047..59482b098a 100644 --- a/react/features/polls/components/native/PollCreate.tsx +++ b/react/features/polls/components/native/PollCreate.tsx @@ -122,8 +122,7 @@ const PollCreate = (props: AbstractProps) => { maxLength = { CHAR_LIMIT } onChange = { name => setAnswer(index, { - name, - voters: [] + name }) } onKeyPress = { ev => onAnswerKeyDown(index, ev) } placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) } diff --git a/react/features/polls/components/native/PollResults.tsx b/react/features/polls/components/native/PollResults.tsx index 2ba53e7d63..f141147e90 100644 --- a/react/features/polls/components/native/PollResults.tsx +++ b/react/features/polls/components/native/PollResults.tsx @@ -65,11 +65,11 @@ const PollResults = (props: AbstractProps) => { { voters && voterCount > 0 && {/* @ts-ignore */} - {voters.map(({ id, name: voterName }) => + {voters.map(voter => ( - { voterName } + { voter.name } ) )} } diff --git a/react/features/polls/components/web/PollAnswer.tsx b/react/features/polls/components/web/PollAnswer.tsx index 8f7d782c20..b1acd3e47f 100644 --- a/react/features/polls/components/web/PollAnswer.tsx +++ b/react/features/polls/components/web/PollAnswer.tsx @@ -62,7 +62,6 @@ const PollAnswer = ({ creatorName, checkBoxStates, poll, - pollId, setCheckbox, setCreateMode, skipAnswer, @@ -77,12 +76,14 @@ const PollAnswer = ({ const { classes } = useStyles(); return ( -
+
{ pollSaved && dispatch(removePoll(pollId, poll)) } + onClick = { () => dispatch(removePoll(poll)) } role = 'button' src = { IconCloseLarge } tabIndex = { 0 } /> @@ -104,6 +105,7 @@ const PollAnswer = ({ setCheckbox(index, ev.target.checked) } /> @@ -120,11 +122,11 @@ const PollAnswer = ({ labelKey = { 'polls.answer.edit' } onClick = { () => { setCreateMode(true); - dispatch(editPoll(pollId, true)); + dispatch(editPoll(poll.pollId, true)); } } type = { BUTTON_TYPES.SECONDARY } />