mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
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:
@@ -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": {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user