feat(polls): rework (#14645)

* feat(polls/web/native): rework polls to manage editing and saving before sending to other participants
This commit is contained in:
Calinteodor
2024-05-22 15:03:24 +03:00
committed by GitHub
parent ef387c939a
commit 3ecc16dc87
19 changed files with 379 additions and 110 deletions

View File

@@ -865,6 +865,8 @@
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
"edit": "Edit",
"send": "Send",
"skip": "Skip",
"submit": "Submit"
},
@@ -878,6 +880,7 @@
"pollQuestion": "Poll Question",
"questionPlaceholder": "Ask a question",
"removeOption": "Remove option",
"save": "Save",
"send": "Send"
},
"errors": {

View File

@@ -18,6 +18,17 @@ export const CHANGE_VOTE = 'CHANGE_VOTE';
*/
export const CLEAR_POLLS = 'CLEAR_POLLS';
/**
* The type of the action triggered when the poll is editing.
*
* {
* type: EDIT_POLL,
* pollId: string,
* editing: boolean
* }
*/
export const EDIT_POLL = 'EDIT_POLL';
/**
* The type of the action which signals that a new Poll was received.
*
@@ -71,3 +82,15 @@ export const RETRACT_VOTE = 'RETRACT_VOTE';
* }
*/
export const RESET_NB_UNREAD_POLLS = 'RESET_NB_UNREAD_POLLS';
/**
* The type of the action triggered when the poll is saved.
*
* {
* type: SAVE_POLL,
* poll: Poll,
* pollId: string,
* saved: boolean
* }
*/
export const SAVE_POLL = 'SAVE_POLL';

View File

@@ -1,11 +1,13 @@
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -128,3 +130,46 @@ export function resetNbUnreadPollsMessages() {
type: RESET_NB_UNREAD_POLLS
};
}
/**
* 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 {boolean} saved - Whether the poll is saved or not.
* @returns {{
* type: RECEIVE_POLL,
* poll: IPoll,
* pollId: string,
* saved: boolean
* }}
*/
export function savePoll(pollId: string, poll: IPoll, saved: boolean) {
return {
type: SAVE_POLL,
pollId,
poll: {
...poll,
saved
}
};
}
/**
* Action to signal editing a poll.
*
* @param {string} pollId - The id of the poll that gets to be edited.
* @param {boolean} editing - Whether the poll is in edit mode or not.
* @returns {{
* type: RECEIVE_POLL,
* pollId: string,
* editing: boolean
* }}
*/
export function editPoll(pollId: string, editing: boolean) {
return {
type: EDIT_POLL,
pollId,
editing
};
}

View File

@@ -7,8 +7,8 @@ import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { getParticipantDisplayName } from '../../base/participants/functions';
import { useBoundSelector } from '../../base/util/hooks';
import { registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL } from '../constants';
import { editPoll, registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL, COMMAND_NEW_POLL } from '../constants';
import { IPoll } from '../types';
/**
@@ -16,6 +16,7 @@ import { IPoll } from '../types';
*/
type InputProps = {
pollId: string;
setCreateMode: (mode: boolean) => void;
};
/*
@@ -26,7 +27,10 @@ export type AbstractProps = {
checkBoxStates: boolean[];
creatorName: string;
poll: IPoll;
pollId: string;
sendPoll: () => void;
setCheckbox: Function;
setCreateMode: (mode: boolean) => void;
skipAnswer: () => void;
skipChangeVote: () => void;
submitAnswer: () => void;
@@ -42,21 +46,23 @@ export type AbstractProps = {
*/
const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { pollId } = props;
const { pollId, setCreateMode } = props;
const conference: any = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const poll: IPoll = useSelector((state: IReduxState) => state['features/polls'].polls[pollId]);
const { answers, lastVote, question, senderId } = poll;
const [ checkBoxStates, setCheckBoxState ] = useState(() => {
if (poll.lastVote !== null) {
return [ ...poll.lastVote ];
if (lastVote !== null) {
return [ ...lastVote ];
}
return new Array(poll.answers.length).fill(false);
return new Array(answers.length).fill(false);
});
const participantName = useBoundSelector(getParticipantDisplayName, poll.senderId);
const participantName = useBoundSelector(getParticipantDisplayName, senderId);
const setCheckbox = useCallback((index, state) => {
const newCheckBoxStates = [ ...checkBoxStates ];
@@ -81,10 +87,21 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
return false;
}, [ pollId, checkBoxStates, conference ]);
const sendPoll = useCallback(() => {
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId,
question,
answers
});
dispatch(editPoll(pollId, false));
}, [ conference, question, answers ]);
const skipAnswer = useCallback(() => {
dispatch(registerVote(pollId, null));
sendAnalytics(createPollEvent('vote.skipped'));
}, [ pollId ]);
const skipChangeVote = useCallback(() => {
@@ -97,7 +114,10 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
checkBoxStates = { checkBoxStates }
creatorName = { participantName }
poll = { poll }
pollId = { pollId }
sendPoll = { sendPoll }
setCheckbox = { setCheckbox }
setCreateMode = { setCreateMode }
skipAnswer = { skipAnswer }
skipChangeVote = { skipChangeVote }
submitAnswer = { submitAnswer }

View File

@@ -1,12 +1,16 @@
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
/* eslint-disable arrow-body-style */
import React, { ComponentType, FormEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { COMMAND_NEW_POLL } from '../constants';
import { getLocalParticipant } from '../../base/participants/functions';
import { savePoll } from '../actions';
import { hasIdenticalAnswers } from '../functions';
import { IPoll } from '../types';
/**
* The type of the React {@code Component} props of inheriting component.
@@ -22,6 +26,8 @@ type InputProps = {
export type AbstractProps = InputProps & {
addAnswer: (index?: number) => void;
answers: Array<string>;
editingPoll: IPoll | undefined;
editingPollId: string | undefined;
isSubmitDisabled: boolean;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
question: string;
@@ -44,9 +50,33 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
const { setCreateMode } = props;
const [ question, setQuestion ] = useState('');
const pollState = useSelector((state: IReduxState) => state['features/polls'].polls);
const [ answers, setAnswers ] = useState([ '', '' ]);
const editingPoll: [ string, IPoll ] | null = useMemo(() => {
if (!pollState) {
return null;
}
for (const key in pollState) {
if (pollState.hasOwnProperty(key) && pollState[key].editing) {
return [ key, pollState[key] ];
}
}
return null;
}, [ pollState ]);
const answerResults = useMemo(() => {
return editingPoll ? editingPoll[1].answers as Array<string> : [ '', '' ];
}, [ editingPoll ]);
const questionResult = useMemo(() => {
return editingPoll ? editingPoll[1].question : '';
}, [ editingPoll ]);
const [ question, setQuestion ] = useState(questionResult);
const [ answers, setAnswers ] = useState(answerResults);
const setAnswer = useCallback((i, answer) => {
setAnswers(currentAnswers => {
@@ -59,11 +89,11 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
}, [ answers ]);
const addAnswer = useCallback((i?: number) => {
const newAnswers = [ ...answers ];
const newAnswers: (string | { name: string; voters: Array<string>; })[] = [ ...answers ];
sendAnalytics(createPollEvent('option.added'));
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
setAnswers(newAnswers);
setAnswers(newAnswers as string[]);
}, [ answers ]);
const removeAnswer = useCallback(i => {
@@ -74,28 +104,45 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
sendAnalytics(createPollEvent('option.removed'));
newAnswers.splice(i, 1);
setAnswers(newAnswers);
setAnswers(newAnswers as string[]);
}, [ answers ]);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const dispatch = useDispatch();
const pollId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const localParticipant = useSelector(getLocalParticipant);
const onSubmit = useCallback(ev => {
if (ev) {
ev.preventDefault();
}
const filteredAnswers = answers.filter(answer => answer.trim().length > 0);
const filteredAnswers = answers.filter((answer: string) => answer.trim().length > 0);
if (filteredAnswers.length < 2) {
return;
}
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
const poll = {
changingVote: false,
senderId: localParticipant?.id,
showResults: false,
lastVote: null,
question,
answers: filteredAnswers
});
answers: filteredAnswers,
saved: false,
editing: false
};
if (editingPoll) {
dispatch(savePoll(editingPoll[0], poll, true));
} else {
dispatch(savePoll(pollId, poll, true));
}
sendAnalytics(createPollEvent('created'));
setCreateMode(false);
@@ -105,7 +152,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
// Check if the poll create form can be submitted i.e. if the send button should be disabled.
const isSubmitDisabled
= question.trim().length <= 0 // If no question is provided
|| answers.filter(answer => answer.trim().length > 0).length < 2 // If not enough options are provided
|| answers.filter((answer: string) => answer.trim().length > 0).length < 2 // If not enough options are provided
|| hasIdenticalAnswers(answers); // If duplicate options are provided
const { t } = useTranslation();
@@ -113,6 +160,8 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
return (<Component
addAnswer = { addAnswer }
answers = { answers }
editingPoll = { editingPoll?.[1] }
editingPollId = { editingPoll?.[0] }
isSubmitDisabled = { isSubmitDisabled }
onSubmit = { onSubmit }
question = { question }

View File

@@ -65,16 +65,17 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
const answers: Array<AnswerInfo> = useMemo(() => {
const allVoters = new Set();
const pollDetailsAnswers = pollDetails.answers as { name: string; voters: string[]; }[];
// Getting every voters ID that participates to the poll
for (const answer of pollDetails.answers) {
for (const answer of pollDetailsAnswers) {
// checking if the voters is an array for supporting old structure model
const voters = answer.voters?.length ? answer.voters : Object.keys(answer.voters);
const voters: string[] = answer.voters.length ? answer.voters : Object.keys(answer.voters);
voters.forEach(voter => allVoters.add(voter));
voters.forEach((voter: string) => allVoters.add(voter));
}
return pollDetails.answers.map(answer => {
return pollDetailsAnswers.map(answer => {
const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0;
const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0;

View File

@@ -1,11 +1,14 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import Switch from '../../../base/ui/components/native/Switch';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
@@ -16,15 +19,20 @@ const PollAnswer = (props: AbstractProps) => {
const {
checkBoxStates,
poll,
pollId,
sendPoll,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
submitAnswer,
t
} = props;
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const localParticipant = useSelector(getLocalParticipant);
const { PRIMARY, SECONDARY } = BUTTON_TYPES;
const pollAnswers = poll.answers as { name: string; voters: string[]; }[];
return (
<>
@@ -34,33 +42,57 @@ const PollAnswer = (props: AbstractProps) => {
}
</Text>
<View style = { chatStyles.answerContent as ViewStyle }>
{poll.answers.map((answer, index) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>{answer.name}</Text>
{
pollAnswers.map((answer, index: number) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
disabled = { poll.saved }
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>
{ poll.saved ? answer : answer.name }
</Text>
</View>
))
}
</View>
{
pollSaved
? <View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.edit'
labelKey = 'polls.answer.edit'
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.send'
labelKey = 'polls.answer.send'
onClick = { sendPoll }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
))}
</View>
<View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
: <View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
}
</>
);
};

View File

@@ -2,12 +2,14 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, Platform, View, ViewStyle } from 'react-native';
import { TextInput } from 'react-native-gesture-handler';
import { Divider } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles
from '../../../settings/components/native/styles';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
@@ -17,6 +19,8 @@ const PollCreate = (props: AbstractProps) => {
const {
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -28,6 +32,7 @@ const PollCreate = (props: AbstractProps) => {
} = props;
const answerListRef = useRef<FlatList>(null);
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -164,13 +169,18 @@ const PollCreate = (props: AbstractProps) => {
<Button
accessibilityLabel = 'polls.create.cancel'
labelKey = 'polls.create.cancel'
onClick = { () => setCreateMode(false) }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.create.send'
accessibilityLabel = 'polls.create.save'
disabled = { isSubmitDisabled }
labelKey = 'polls.create.send'
labelKey = 'polls.create.save'
onClick = { onSubmit }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />

View File

@@ -15,9 +15,14 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = ({ pollId }: IProps) => {
const PollItem = ({ pollId, setCreateMode }: IProps) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
@@ -28,7 +33,8 @@ const PollItem = ({ pollId }: IProps) => {
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</View>

View File

@@ -12,7 +12,11 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import PollItem from './PollItem';
import { chatStyles } from './styles';
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
const { t } = useTranslation();
const listPolls = Object.keys(polls);
@@ -20,7 +24,8 @@ const PollsList = () => {
const renderItem = useCallback(({ item }) => (
<PollItem
key = { item }
pollId = { item } />)
pollId = { item }
setCreateMode = { setCreateMode } />)
, []);
const flatlistRef = useRef<FlatList>(null);

View File

@@ -53,21 +53,20 @@ const PollsPane = (props: AbstractProps) => {
{
createMode
? <PollCreate setCreateMode = { setCreateMode } />
: <PollsList />
}
{
!createMode && <Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
: <>
<PollsList setCreateMode = { setCreateMode } />
<Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
</>
}
</JitsiScreen>
);
};
/*
* We apply AbstractPollsPane to fill in the AbstractProps common
* to both the web and native implementations.

View File

@@ -1,10 +1,14 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Checkbox from '../../../base/ui/components/web/Checkbox';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
@@ -53,13 +57,18 @@ const PollAnswer = ({
creatorName,
checkBoxStates,
poll,
pollId,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
sendPoll,
submitAnswer,
t
}: AbstractProps) => {
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const { classes } = useStyles();
return (
@@ -80,26 +89,44 @@ const PollAnswer = ({
key = { index }>
<Checkbox
checked = { checkBoxStates[index] }
disabled = { poll.saved }
key = { index }
label = { answer.name }
// eslint-disable-next-line react/jsx-no-bind
label = { poll.saved ? answer : answer.name }
onChange = { ev => setCheckbox(index, ev.target.checked) } />
</li>
))
}
</ul>
<div className = { classes.footer } >
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
{
pollSaved ? <>
<Button
accessibilityLabel = { t('polls.answer.edit') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.edit' }
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.send') }
labelKey = { 'polls.answer.send' }
onClick = { sendPoll } />
</> : <>
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
</>
}
</div>
</div>
);

View File

@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
@@ -64,6 +66,8 @@ const useStyles = makeStyles()(theme => {
const PollCreate = ({
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -74,6 +78,7 @@ const PollCreate = ({
t
}: AbstractProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -201,7 +206,7 @@ const PollCreate = ({
value = { question } />
</div>
<ol className = { classes.answerList }>
{answers.map((answer: any, i: number) => {
{answers.map((answer: string, i: number) => {
const isIdenticalAnswer = answers.slice(0, i).length === 0 ? false
: answers.slice(0, i).some((prevAnswer: string) =>
@@ -252,13 +257,18 @@ const PollCreate = ({
accessibilityLabel = { t('polls.create.cancel') }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => setCreateMode(false) }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.send') }
accessibilityLabel = { t('polls.create.save') }
disabled = { isSubmitDisabled }
isSubmit = { true }
labelKey = { 'polls.create.send' } />
labelKey = { 'polls.create.save' } />
</div>
</form>);
};

View File

@@ -14,9 +14,14 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId, setCreateMode }: IProps, ref) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
@@ -26,7 +31,8 @@ const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, r
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</div>

View File

@@ -39,7 +39,11 @@ const useStyles = makeStyles()(theme => {
};
});
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
@@ -80,7 +84,8 @@ const PollsList = () => {
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
ref = { listPolls.length - 1 === index ? pollListEndRef : null }
setCreateMode = { setCreateMode } />
))}
</>
);

View File

@@ -35,7 +35,7 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
? <PollCreate setCreateMode = { setCreateMode } />
: <div className = { classes.container }>
<div className = { classes.listContainer } >
<PollsList />
<PollsList setCreateMode = { setCreateMode } />
</div>
<div className = { classes.footer }>
<Button

View File

@@ -50,7 +50,9 @@ const parsePollData = (pollData: IPollData): IPoll | null => {
question,
showResults: true,
lastVote: null,
answers
answers,
saved: false,
editing: false
};
};
@@ -121,7 +123,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
switch (data.type) {
case COMMAND_NEW_POLL: {
const { question, answers, pollId, senderId } = data;
const { pollId, answers, senderId, question } = data;
const poll = {
changingVote: false,
@@ -129,12 +131,14 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
showResults: false,
lastVote: null,
question,
answers: answers.map((answer: IAnswer) => {
answers: answers.map((answer: string) => {
return {
name: answer,
voters: []
};
})
}),
saved: false,
editing: false
};
dispatch(receivePoll(pollId, poll, true));

View File

@@ -3,11 +3,13 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -51,20 +53,18 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
// Reducer triggered when a poll is received
case RECEIVE_POLL: {
const newState = {
// Reducer triggered when a poll is received or saved.
case RECEIVE_POLL:
case SAVE_POLL: {
return {
...state,
polls: {
...state.polls,
// The poll is added to the dictionary of received polls
[action.pollId]: action.poll
},
nbUnreadPolls: state.nbUnreadPolls + 1
};
return newState;
}
// Reducer triggered when an answer is received
@@ -81,7 +81,8 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
}
// if the poll exists, we update it with the incoming answer
const newAnswers = state.polls[pollId].answers
const statePollsAnswers = state.polls[pollId].answers as { name: string; voters: string[]; }[];
const newAnswers = statePollsAnswers
.map(_answer => {
// checking if the voters is an array for supporting old structure model
const answerVoters = _answer.voters
@@ -162,6 +163,19 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
case EDIT_POLL: {
return {
...state,
polls: {
...state.polls,
[action.pollId]: {
...state.polls[action.pollId],
editing: action.editing
}
}
};
}
default:
return state;
}

View File

@@ -27,13 +27,18 @@ export interface IPoll {
* An array of answers:
* the name of the answer name and a map of ids and names of voters voting for this option.
*/
answers: Array<{ name: string; voters: Array<string>; }>;
answers: Array<{ name: string; voters: string[]; }> | string[];
/**
* Whether the poll vote is being edited/changed.
*/
changingVote: boolean;
/**
* Whether poll is in edit mode or not?.
*/
editing: boolean;
/**
* The last sent votes for this poll, or null if voting was skipped
* Note: This is reset when voting/skipping, not when clicking "Change vote".
@@ -45,10 +50,15 @@ export interface IPoll {
*/
question: string;
/**
* Whether poll is saved or not?.
*/
saved: boolean;
/**
* ID of the sender of this poll.
*/
senderId: string;
senderId: string | undefined;
/**
* Whether the results should be shown instead of the answer form.