mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
feat(polls): Move polls to using a component (#16406)
* squash: Renames module.
* squash: Loads polls component.
* squash: Attach needed logic when components/hosts load.
* squash: Moves to use component.
* squash: Uses json-message format with types.
* squash: Checks for polls support.
* squash: Fixes comments and moves validate polls to backend.
* squash: Fix debian build.
* fix(polls): Fixes polls in breakout rooms.
* squash: Further simplify types.
Separate type that needs to go into ljm and those used only for the UI part.
Simplify answer/voter type to be unified across operations which simplifies and its logic.
* squash: Change voters structure to be {id, name}.
* squash: Update react/features/conference/functions.any.ts
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
* squash: Drops roomJid from messages. Uses the connection information as breakout does.
---------
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
This commit is contained in:
@@ -107,6 +107,7 @@ export interface IJitsiConference {
|
||||
getParticipantById: Function;
|
||||
getParticipantCount: Function;
|
||||
getParticipants: Function;
|
||||
getPolls: Function;
|
||||
getRole: Function;
|
||||
getShortTermCredentials: Function;
|
||||
getSpeakerStats: () => ISpeakerStats;
|
||||
|
||||
@@ -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 = ({
|
||||
<input
|
||||
checked = { checked }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
name = { name }
|
||||
onChange = { onChange }
|
||||
type = 'checkbox' />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<AbstractProps>) => (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<AbstractProps>) => (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<AbstractProps>) => (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<AbstractProps>) => (props:
|
||||
checkBoxStates = { checkBoxStates }
|
||||
creatorName = { participantName }
|
||||
poll = { poll }
|
||||
pollId = { pollId }
|
||||
sendPoll = { sendPoll }
|
||||
setCheckbox = { setCheckbox }
|
||||
setCreateMode = { setCreateMode }
|
||||
|
||||
@@ -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<IAnswerData>;
|
||||
editingPoll: IPoll | undefined;
|
||||
editingPoll: IPollData | undefined;
|
||||
editingPollId: string | undefined;
|
||||
isSubmitDisabled: boolean;
|
||||
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
|
||||
@@ -52,7 +52,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (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<AbstractProps>) => (props:
|
||||
? editingPoll[1].answers
|
||||
: [
|
||||
{
|
||||
name: '',
|
||||
voters: []
|
||||
name: ''
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
voters: []
|
||||
name: ''
|
||||
} ];
|
||||
}, [ editingPoll ]);
|
||||
|
||||
@@ -104,8 +102,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (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<AbstractProps>) => (props:
|
||||
return;
|
||||
}
|
||||
|
||||
const poll = {
|
||||
dispatch(savePoll({
|
||||
changingVote: false,
|
||||
senderId: localParticipant?.id,
|
||||
showResults: false,
|
||||
@@ -148,14 +145,9 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (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'));
|
||||
|
||||
|
||||
@@ -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<HTMLButtonElement> | 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<AbstractProps>) => (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<AbstractProps>) => (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<IVoterData>);
|
||||
|
||||
return {
|
||||
name: answer.name,
|
||||
percentage,
|
||||
voters,
|
||||
voters: voters,
|
||||
voterCount: nrOfVotersPerAnswer
|
||||
};
|
||||
});
|
||||
@@ -113,8 +106,9 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
|
||||
<Component
|
||||
answers = { answers }
|
||||
changeVote = { changeVote }
|
||||
creatorName = { participant ? participant.name : '' }
|
||||
creatorName = { creatorName }
|
||||
haveVoted = { poll.lastVote !== null }
|
||||
pollId = { pollId }
|
||||
question = { poll.question }
|
||||
showDetails = { showDetails }
|
||||
t = { t }
|
||||
|
||||
@@ -20,7 +20,6 @@ const PollAnswer = (props: AbstractProps) => {
|
||||
const {
|
||||
checkBoxStates,
|
||||
poll,
|
||||
pollId,
|
||||
sendPoll,
|
||||
setCheckbox,
|
||||
setCreateMode,
|
||||
@@ -46,7 +45,7 @@ const PollAnswer = (props: AbstractProps) => {
|
||||
</View>
|
||||
{
|
||||
pollSaved && <IconButton
|
||||
onPress = { () => dispatch(removePoll(pollId, poll)) }
|
||||
onPress = { () => dispatch(removePoll(poll)) }
|
||||
src = { IconCloseLarge } />
|
||||
}
|
||||
</View>
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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 }) }
|
||||
|
||||
@@ -65,11 +65,11 @@ const PollResults = (props: AbstractProps) => {
|
||||
{ voters && voterCount > 0
|
||||
&& <View style = { resultsStyles.voters as ViewStyle }>
|
||||
{/* @ts-ignore */}
|
||||
{voters.map(({ id, name: voterName }) =>
|
||||
{voters.map(voter =>
|
||||
(<Text
|
||||
key = { id }
|
||||
key = { voter.id }
|
||||
style = { resultsStyles.voter as TextStyle }>
|
||||
{ voterName }
|
||||
{ voter.name }
|
||||
</Text>)
|
||||
)}
|
||||
</View>}
|
||||
|
||||
@@ -62,7 +62,6 @@ const PollAnswer = ({
|
||||
creatorName,
|
||||
checkBoxStates,
|
||||
poll,
|
||||
pollId,
|
||||
setCheckbox,
|
||||
setCreateMode,
|
||||
skipAnswer,
|
||||
@@ -77,12 +76,14 @@ const PollAnswer = ({
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = { `poll-${poll.pollId}` }>
|
||||
{
|
||||
pollSaved && <Icon
|
||||
ariaLabel = { t('polls.closeButton') }
|
||||
className = { classes.closeBtn }
|
||||
onClick = { () => dispatch(removePoll(pollId, poll)) }
|
||||
onClick = { () => dispatch(removePoll(poll)) }
|
||||
role = 'button'
|
||||
src = { IconCloseLarge }
|
||||
tabIndex = { 0 } />
|
||||
@@ -104,6 +105,7 @@ const PollAnswer = ({
|
||||
<Checkbox
|
||||
checked = { checkBoxStates[index] }
|
||||
disabled = { poll.saved }
|
||||
id = { `poll-answer-checkbox-${poll.pollId}-${index}` }
|
||||
key = { index }
|
||||
label = { answer.name }
|
||||
onChange = { ev => 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 } />
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.answer.send') }
|
||||
accessibilityLabel = { t('polls.create.accessibilityLabel.send') }
|
||||
labelKey = { 'polls.answer.send' }
|
||||
onClick = { sendPoll } />
|
||||
</> : <>
|
||||
|
||||
@@ -223,8 +223,7 @@ const PollCreate = ({
|
||||
label = { t('polls.create.pollOption', { index: i + 1 }) }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { name => setAnswer(i, {
|
||||
name,
|
||||
voters: []
|
||||
name
|
||||
}) }
|
||||
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
|
||||
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
|
||||
@@ -235,6 +234,7 @@ const PollCreate = ({
|
||||
{ answers.length > 2
|
||||
&& <button
|
||||
className = { classes.removeOption }
|
||||
data-testid = { `remove-polls-answer-input-${i}` }
|
||||
onClick = { () => removeAnswer(i) }
|
||||
type = 'button'>
|
||||
{ t('polls.create.removeOption') }
|
||||
|
||||
@@ -113,6 +113,7 @@ const PollResults = ({
|
||||
changeVote,
|
||||
creatorName,
|
||||
haveVoted,
|
||||
pollId,
|
||||
showDetails,
|
||||
question,
|
||||
t,
|
||||
@@ -121,7 +122,9 @@ const PollResults = ({
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = { `poll-${pollId}` }>
|
||||
<div className = { classes.header }>
|
||||
<div className = { classes.question }>
|
||||
{question}
|
||||
@@ -136,7 +139,9 @@ const PollResults = ({
|
||||
<div className = { classes.answerName }>
|
||||
{name}
|
||||
</div>
|
||||
<div className = { classes.answerResultContainer }>
|
||||
<div
|
||||
className = { classes.answerResultContainer }
|
||||
id = { `poll-result-${pollId}-${index}` }>
|
||||
<span className = { classes.barContainer }>
|
||||
<div
|
||||
className = { classes.bar }
|
||||
@@ -148,8 +153,8 @@ const PollResults = ({
|
||||
</div>
|
||||
{showDetails && voters && voterCount > 0
|
||||
&& <ul className = { classes.voters }>
|
||||
{voters.map(voter =>
|
||||
<li key = { voter?.id }>{voter?.name}</li>
|
||||
{ voters.map(voter =>
|
||||
<li key = { voter.id }>{ voter.name }</li>
|
||||
)}
|
||||
</ul>}
|
||||
</li>)
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
export const COMMAND_NEW_POLL = 'new-poll';
|
||||
export const COMMAND_ANSWER_POLL = 'answer-poll';
|
||||
export const COMMAND_OLD_POLLS = 'old-polls';
|
||||
|
||||
export const CHAR_LIMIT = 500;
|
||||
export const ANSWERS_LIMIT = 255;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { ENDPOINT_MESSAGE_RECEIVED, NON_PARTICIPANT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantById, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound } from '../base/sounds/actions';
|
||||
@@ -11,13 +12,7 @@ import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/c
|
||||
|
||||
import { RECEIVE_POLL } from './actionTypes';
|
||||
import { clearPolls, receiveAnswer, receivePoll } from './actions';
|
||||
import {
|
||||
COMMAND_ANSWER_POLL,
|
||||
COMMAND_NEW_POLL,
|
||||
COMMAND_OLD_POLLS
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IAnswer, IPoll, IPollData } from './types';
|
||||
import { IIncomingAnswerData } from './types';
|
||||
|
||||
/**
|
||||
* The maximum number of answers a poll can have.
|
||||
@@ -28,75 +23,29 @@ const MAX_ANSWERS = 32;
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. Clear messages or close the chat modal if it's left
|
||||
* open.
|
||||
* When joining new conference set up the listeners for polls.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
(conference, { dispatch, getState }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(clearPolls());
|
||||
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.POLL_RECEIVED, (data: any) => {
|
||||
_handleReceivedPollsData(data, dispatch, getState);
|
||||
});
|
||||
conference.on(JitsiConferenceEvents.POLL_ANSWER_RECEIVED, (data: any) => {
|
||||
_handleReceivedPollsAnswer(data, dispatch, getState);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsePollData = (pollData: Partial<IPollData>): IPoll | null => {
|
||||
if (typeof pollData !== 'object' || pollData === null) {
|
||||
return null;
|
||||
}
|
||||
const { id, senderId, question, answers } = pollData;
|
||||
|
||||
if (typeof id !== 'string' || typeof senderId !== 'string'
|
||||
|| typeof question !== 'string' || !(answers instanceof Array)) {
|
||||
logger.error('Malformed poll data received:', pollData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate answers.
|
||||
if (answers.some(answer => typeof answer !== 'string')) {
|
||||
logger.error('Malformed answers data received:', answers);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
changingVote: false,
|
||||
senderId,
|
||||
question,
|
||||
showResults: true,
|
||||
lastVote: null,
|
||||
answers,
|
||||
saved: false,
|
||||
editing: false
|
||||
};
|
||||
};
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? participant.getId() : undefined,
|
||||
voterId: isNewPoll ? undefined : participant.getId()
|
||||
}, dispatch, getState);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
|
||||
const { id, json: data } = action;
|
||||
const isNewPoll = data.type === COMMAND_NEW_POLL;
|
||||
|
||||
_handleReceivePollsMessage({
|
||||
...data,
|
||||
senderId: isNewPoll ? id : undefined,
|
||||
voterId: isNewPoll ? undefined : id
|
||||
}, dispatch, getState);
|
||||
break;
|
||||
}
|
||||
|
||||
case RECEIVE_POLL: {
|
||||
const state = getState();
|
||||
@@ -120,7 +69,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles receiving of polls message command.
|
||||
* Handles receiving of new or history polls to load.
|
||||
*
|
||||
* @param {Object} data - The json data carried by the polls message.
|
||||
* @param {Function} dispatch - The dispatch function.
|
||||
@@ -128,82 +77,58 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
function _handleReceivedPollsData(data: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
if (arePollsDisabled(getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
const { pollId, answers, senderId, question, history } = data;
|
||||
const poll = {
|
||||
changingVote: false,
|
||||
senderId,
|
||||
showResults: false,
|
||||
lastVote: null,
|
||||
question,
|
||||
answers: answers.slice(0, MAX_ANSWERS),
|
||||
saved: false,
|
||||
editing: false,
|
||||
pollId
|
||||
};
|
||||
|
||||
case COMMAND_NEW_POLL: {
|
||||
const { pollId, answers, senderId, question } = data;
|
||||
const tmp = {
|
||||
id: pollId,
|
||||
answers,
|
||||
question,
|
||||
senderId
|
||||
};
|
||||
dispatch(receivePoll(poll, !history));
|
||||
|
||||
// Check integrity of the poll data.
|
||||
// TODO(saghul): we should move this to the server side, likely by storing the
|
||||
// poll data in the room metadata.
|
||||
if (parsePollData(tmp) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const poll = {
|
||||
changingVote: false,
|
||||
senderId,
|
||||
showResults: false,
|
||||
lastVote: null,
|
||||
question,
|
||||
answers: answers.map((answer: string) => {
|
||||
return {
|
||||
name: answer,
|
||||
voters: []
|
||||
};
|
||||
}).slice(0, MAX_ANSWERS),
|
||||
saved: false,
|
||||
editing: false
|
||||
};
|
||||
|
||||
dispatch(receivePoll(pollId, poll, true));
|
||||
if (!history) {
|
||||
dispatch(showNotification({
|
||||
appearance: NOTIFICATION_TYPE.NORMAL,
|
||||
titleKey: 'polls.notification.title',
|
||||
descriptionKey: 'polls.notification.description'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
case COMMAND_ANSWER_POLL: {
|
||||
const { pollId, answers, voterId } = data;
|
||||
|
||||
const receivedAnswer: IAnswer = {
|
||||
voterId,
|
||||
pollId,
|
||||
answers: answers.slice(0, MAX_ANSWERS).map(Boolean)
|
||||
};
|
||||
|
||||
dispatch(receiveAnswer(pollId, receivedAnswer));
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
case COMMAND_OLD_POLLS: {
|
||||
const { polls } = data;
|
||||
|
||||
for (const pollData of polls) {
|
||||
const poll = parsePollData(pollData);
|
||||
|
||||
if (poll === null) {
|
||||
logger.warn('Malformed old poll data', pollData);
|
||||
} else {
|
||||
dispatch(receivePoll(pollData.id, poll, false));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles receiving of pools answers.
|
||||
*
|
||||
* @param {Object} data - The json data carried by the polls message.
|
||||
* @param {Function} dispatch - The dispatch function.
|
||||
* @param {Function} getState - The getState function.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedPollsAnswer(data: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
if (arePollsDisabled(getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { pollId, answers, senderId, senderName } = data;
|
||||
|
||||
const receivedAnswer: IIncomingAnswerData = {
|
||||
answers: answers.slice(0, MAX_ANSWERS).map(Boolean),
|
||||
pollId,
|
||||
senderId,
|
||||
voterName: getParticipantById(getState(), senderId)
|
||||
? getParticipantDisplayName(getState(), senderId) : senderName
|
||||
};
|
||||
|
||||
dispatch(receiveAnswer(receivedAnswer));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
RESET_NB_UNREAD_POLLS,
|
||||
SAVE_POLL
|
||||
} from './actionTypes';
|
||||
import { IAnswer, IPoll } from './types';
|
||||
import { IIncomingAnswerData, IPollData } from './types';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
polls: {},
|
||||
@@ -23,7 +23,7 @@ const INITIAL_STATE = {
|
||||
export interface IPollsState {
|
||||
nbUnreadPolls: number;
|
||||
polls: {
|
||||
[pollId: string]: IPoll;
|
||||
[pollId: string]: IPollData;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
...state,
|
||||
polls: {
|
||||
...state.polls,
|
||||
[action.pollId]: action.poll
|
||||
[action.poll.pollId]: action.poll
|
||||
},
|
||||
nbUnreadPolls: state.nbUnreadPolls + 1
|
||||
};
|
||||
@@ -72,7 +72,7 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
...state,
|
||||
polls: {
|
||||
...state.polls,
|
||||
[action.pollId]: action.poll
|
||||
[action.poll.pollId]: action.poll
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -81,7 +81,9 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
// The answer is added to an existing poll
|
||||
case RECEIVE_ANSWER: {
|
||||
|
||||
const { pollId, answer }: { answer: IAnswer; pollId: string; } = action;
|
||||
const { answer }: { answer: IIncomingAnswerData; } = action;
|
||||
const pollId = answer.pollId;
|
||||
const poll = state.polls[pollId];
|
||||
|
||||
// if the poll doesn't exist
|
||||
if (!(pollId in state.polls)) {
|
||||
@@ -91,33 +93,22 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
}
|
||||
|
||||
// if the poll exists, we update it with the incoming answer
|
||||
const newAnswers = state.polls[pollId].answers
|
||||
.map(_answer => {
|
||||
// checking if the voters is an array for supporting old structure model
|
||||
const answerVoters = _answer.voters
|
||||
? _answer.voters.length
|
||||
? [ ..._answer.voters ] : Object.keys(_answer.voters) : [];
|
||||
|
||||
return {
|
||||
name: _answer.name,
|
||||
voters: answerVoters
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
for (let i = 0; i < newAnswers.length; i++) {
|
||||
for (let i = 0; i < poll.answers.length; i++) {
|
||||
// if the answer was chosen, we add the senderId to the array of voters of this answer
|
||||
const voters = newAnswers[i].voters as any;
|
||||
let voters = poll.answers[i].voters || [];
|
||||
|
||||
const index = voters.indexOf(answer.voterId);
|
||||
|
||||
if (answer.answers[i]) {
|
||||
if (index === -1) {
|
||||
voters.push(answer.voterId);
|
||||
if (voters.find(user => user.id === answer.senderId)) {
|
||||
if (!answer.answers[i]) {
|
||||
voters = voters.filter(user => user.id !== answer.senderId);
|
||||
}
|
||||
} else if (index > -1) {
|
||||
voters.splice(index, 1);
|
||||
} else if (answer.answers[i]) {
|
||||
voters.push({
|
||||
id: answer.senderId,
|
||||
name: answer.voterName
|
||||
});
|
||||
}
|
||||
|
||||
poll.answers[i].voters = voters?.length ? voters : undefined;
|
||||
}
|
||||
|
||||
// finally we update the state by returning the updated poll
|
||||
@@ -126,8 +117,8 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
polls: {
|
||||
...state.polls,
|
||||
[pollId]: {
|
||||
...state.polls[pollId],
|
||||
answers: newAnswers
|
||||
...poll,
|
||||
answers: [ ...poll.answers ]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -179,7 +170,7 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [action.pollId]: _removedPoll, ...newState } = state.polls;
|
||||
const { [action.poll.pollId]: _removedPoll, ...newState } = state.polls;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export interface IAnswer {
|
||||
|
||||
/**
|
||||
* TODO: move to ljm.
|
||||
*/
|
||||
export interface IIncomingAnswer {
|
||||
/**
|
||||
* An array of boolean: true if the answer was chosen by the responder, else false.
|
||||
*/
|
||||
@@ -11,16 +13,24 @@ export interface IAnswer {
|
||||
pollId: string;
|
||||
|
||||
/**
|
||||
* ID of the voter for this answer.
|
||||
* ID of the sender of this answer.
|
||||
*/
|
||||
voterId: string;
|
||||
senderId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of IIncomingAnswer with UI only fields.
|
||||
*/
|
||||
export interface IIncomingAnswerData extends IIncomingAnswer {
|
||||
/**
|
||||
* Name of the voter for this answer.
|
||||
*/
|
||||
voterName?: string;
|
||||
voterName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: move to ljm and use it from there.
|
||||
*/
|
||||
export interface IPoll {
|
||||
|
||||
/**
|
||||
@@ -30,7 +40,27 @@ export interface IPoll {
|
||||
answers: Array<IAnswerData>;
|
||||
|
||||
/**
|
||||
* Whether the poll vote is being edited/changed.
|
||||
* The unique ID of this poll.
|
||||
*/
|
||||
pollId: string;
|
||||
|
||||
/**
|
||||
* The question asked by this poll.
|
||||
*/
|
||||
question: string;
|
||||
|
||||
/**
|
||||
* ID of the sender of this poll.
|
||||
*/
|
||||
senderId: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of IPoll with UI only fields.
|
||||
*/
|
||||
export interface IPollData extends IPoll {
|
||||
/**
|
||||
* Whether the poll vote is being edited/changed. UI only, not stored on the backend.
|
||||
*/
|
||||
changingVote: boolean;
|
||||
|
||||
@@ -46,30 +76,35 @@ export interface IPoll {
|
||||
lastVote: Array<boolean> | null;
|
||||
|
||||
/**
|
||||
* The question asked by this poll.
|
||||
*/
|
||||
question: string;
|
||||
|
||||
/**
|
||||
* Whether poll is saved or not?.
|
||||
* Whether poll is saved or not?. UI only, not stored on the backend.
|
||||
*/
|
||||
saved: boolean;
|
||||
|
||||
/**
|
||||
* ID of the sender of this poll.
|
||||
*/
|
||||
senderId: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether the results should be shown instead of the answer form.
|
||||
* UI only, not stored on the backend.
|
||||
*/
|
||||
showResults: boolean;
|
||||
}
|
||||
|
||||
export interface IPollData extends IPoll {
|
||||
/**
|
||||
* TODO: move to ljm and use it from there.
|
||||
*/
|
||||
export interface IVoterData {
|
||||
/**
|
||||
* The id of the voter.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Voter name if voter is not in the meeting.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: move to ljm and use it from there.
|
||||
*/
|
||||
export interface IAnswerData {
|
||||
|
||||
/**
|
||||
@@ -80,5 +115,5 @@ export interface IAnswerData {
|
||||
/**
|
||||
* An array of voters.
|
||||
*/
|
||||
voters: Array<string>;
|
||||
voters?: Array<IVoterData>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user