This commit is contained in:
121
react/features/polls/components/native/PollAnswer.tsx
Normal file
121
react/features/polls/components/native/PollAnswer.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import IconButton from '../../../base/ui/components/native/IconButton';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { editPoll, removePoll } from '../../actions';
|
||||
import { isSubmitAnswerDisabled } from '../../functions';
|
||||
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
|
||||
|
||||
import { dialogStyles, pollsStyles } from './styles';
|
||||
|
||||
const PollAnswer = (props: AbstractProps) => {
|
||||
const {
|
||||
checkBoxStates,
|
||||
poll,
|
||||
pollId,
|
||||
sendPoll,
|
||||
setCheckbox,
|
||||
setCreateMode,
|
||||
skipAnswer,
|
||||
skipChangeVote,
|
||||
submitAnswer,
|
||||
t
|
||||
} = props;
|
||||
const { changingVote, saved: pollSaved } = poll;
|
||||
const dispatch = useDispatch();
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const { PRIMARY, SECONDARY } = BUTTON_TYPES;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { dialogStyles.headerContainer as ViewStyle }>
|
||||
<View>
|
||||
<Text style = { dialogStyles.questionText as TextStyle } >{ poll.question }</Text>
|
||||
<Text style = { dialogStyles.questionOwnerText as TextStyle } >{
|
||||
t('polls.by', { name: localParticipant?.name })
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
{
|
||||
pollSaved && <IconButton
|
||||
onPress = { () => dispatch(removePoll(pollId, poll)) }
|
||||
src = { IconCloseLarge } />
|
||||
}
|
||||
</View>
|
||||
<View
|
||||
id = 'answer-content'
|
||||
style = { pollsStyles.answerContent as ViewStyle }>
|
||||
{
|
||||
poll.answers.map((answer, index: number) => (
|
||||
<View
|
||||
key = { index }
|
||||
style = { pollsStyles.switchRow as ViewStyle } >
|
||||
<Switch
|
||||
checked = { checkBoxStates[index] }
|
||||
disabled = { poll.saved }
|
||||
id = 'answer-switch'
|
||||
onChange = { state => setCheckbox(index, state) } />
|
||||
<Text style = { pollsStyles.switchLabel as TextStyle }>
|
||||
{ answer.name }
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
{
|
||||
pollSaved
|
||||
? <View style = { pollsStyles.buttonRow as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.answer.edit'
|
||||
id = { t('polls.answer.edit') }
|
||||
labelKey = 'polls.answer.edit'
|
||||
onClick = { () => {
|
||||
setCreateMode(true);
|
||||
dispatch(editPoll(pollId, true));
|
||||
} }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { SECONDARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'polls.answer.send'
|
||||
id = { t('polls.answer.send') }
|
||||
labelKey = 'polls.answer.send'
|
||||
onClick = { sendPoll }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { PRIMARY } />
|
||||
</View>
|
||||
: <View style = { pollsStyles.buttonRow as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.answer.skip'
|
||||
id = { t('polls.answer.skip') }
|
||||
labelKey = 'polls.answer.skip'
|
||||
onClick = { changingVote ? skipChangeVote : skipAnswer }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { SECONDARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'polls.answer.submit'
|
||||
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
|
||||
id = { t('polls.answer.submit') }
|
||||
labelKey = 'polls.answer.submit'
|
||||
onClick = { submitAnswer }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { PRIMARY } />
|
||||
</View>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollAnswer to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollAnswer(PollAnswer);
|
||||
220
react/features/polls/components/native/PollCreate.tsx
Normal file
220
react/features/polls/components/native/PollCreate.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
|
||||
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 { editPoll } from '../../actions';
|
||||
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
|
||||
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
|
||||
|
||||
import { dialogStyles, pollsStyles } from './styles';
|
||||
|
||||
const PollCreate = (props: AbstractProps) => {
|
||||
const {
|
||||
addAnswer,
|
||||
answers,
|
||||
editingPoll,
|
||||
editingPollId,
|
||||
isSubmitDisabled,
|
||||
onSubmit,
|
||||
question,
|
||||
removeAnswer,
|
||||
setAnswer,
|
||||
setCreateMode,
|
||||
setQuestion,
|
||||
t
|
||||
} = props;
|
||||
|
||||
const answerListRef = useRef<FlatList>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/*
|
||||
* This ref stores the Array of answer input fields, allowing us to focus on them.
|
||||
* This array is maintained by registerFieldRef and the useEffect below.
|
||||
*/
|
||||
const answerInputs = useRef<TextInput[]>([]);
|
||||
const registerFieldRef = useCallback((i, input) => {
|
||||
if (input === null) {
|
||||
return;
|
||||
}
|
||||
answerInputs.current[i] = input;
|
||||
}, [ answerInputs ]);
|
||||
|
||||
useEffect(() => {
|
||||
answerInputs.current = answerInputs.current.slice(0, answers.length);
|
||||
setTimeout(() => {
|
||||
answerListRef.current?.scrollToEnd({ animated: true });
|
||||
}, 1000);
|
||||
}, [ answers ]);
|
||||
|
||||
/*
|
||||
* This state allows us to requestFocus asynchronously, without having to worry
|
||||
* about whether a newly created input field has been rendered yet or not.
|
||||
*/
|
||||
const [ lastFocus, requestFocus ] = useState<number | null>(null);
|
||||
const { PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
|
||||
|
||||
useEffect(() => {
|
||||
if (lastFocus === null) {
|
||||
return;
|
||||
}
|
||||
const input = answerInputs.current[lastFocus];
|
||||
|
||||
if (input === undefined) {
|
||||
return;
|
||||
}
|
||||
input.focus();
|
||||
|
||||
}, [ answerInputs, lastFocus ]);
|
||||
|
||||
|
||||
const onQuestionKeyDown = useCallback(() => {
|
||||
answerInputs.current[0].focus();
|
||||
}, []);
|
||||
|
||||
// Called on keypress in answer fields
|
||||
const onAnswerKeyDown = useCallback((index: number, ev) => {
|
||||
const { key } = ev.nativeEvent;
|
||||
const currentText = answers[index].name;
|
||||
|
||||
if (key === 'Backspace' && currentText === '' && answers.length > 1) {
|
||||
removeAnswer(index);
|
||||
requestFocus(index > 0 ? index - 1 : 0);
|
||||
}
|
||||
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
const createRemoveOptionButton = (onPress: () => void) => (
|
||||
<Button
|
||||
id = { t('polls.create.removeOption') }
|
||||
labelKey = 'polls.create.removeOption'
|
||||
labelStyle = { dialogStyles.optionRemoveButtonText }
|
||||
onClick = { onPress }
|
||||
style = { dialogStyles.optionRemoveButton }
|
||||
type = { TERTIARY } />
|
||||
);
|
||||
|
||||
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
|
||||
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
const renderListItem = ({ index }: { index: number; }) => {
|
||||
|
||||
const isIdenticalAnswer
|
||||
= answers.slice(0, index).length === 0 ? false : answers.slice(0, index).some(prevAnswer =>
|
||||
prevAnswer.name === answers[index].name
|
||||
&& prevAnswer.name !== '' && answers[index].name !== '');
|
||||
|
||||
return (
|
||||
<View
|
||||
id = 'option-container'
|
||||
style = { dialogStyles.optionContainer as ViewStyle }>
|
||||
<Input
|
||||
blurOnSubmit = { false }
|
||||
bottomLabel = { (
|
||||
isIdenticalAnswer ? t('polls.errors.notUniqueOption', { index: index + 1 }) : '') }
|
||||
error = { isIdenticalAnswer }
|
||||
id = { `polls-answer-input-${index}` }
|
||||
label = { t('polls.create.pollOption', { index: index + 1 }) }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { name => setAnswer(index,
|
||||
{
|
||||
name,
|
||||
voters: []
|
||||
}) }
|
||||
onKeyPress = { ev => onAnswerKeyDown(index, ev) }
|
||||
placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) }
|
||||
|
||||
// This is set to help the touch event not be propagated to any subviews.
|
||||
pointerEvents = { 'auto' }
|
||||
ref = { input => registerFieldRef(index, input) }
|
||||
value = { answers[index].name } />
|
||||
{
|
||||
answers.length > 2
|
||||
&& createRemoveOptionButton(() => removeAnswer(index))
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderListHeaderComponent = useMemo(() => (
|
||||
<>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
blurOnSubmit = { false }
|
||||
customStyles = {{ container: dialogStyles.customContainer }}
|
||||
id = { t('polls.create.pollQuestion') }
|
||||
label = { t('polls.create.pollQuestion') }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { setQuestion }
|
||||
onSubmitEditing = { onQuestionKeyDown }
|
||||
placeholder = { t('polls.create.questionPlaceholder') }
|
||||
|
||||
// This is set to help the touch event not be propagated to any subviews.
|
||||
pointerEvents = { 'auto' }
|
||||
value = { question } />
|
||||
<Divider style = { pollsStyles.fieldSeparator as ViewStyle } />
|
||||
</>
|
||||
), [ question ]);
|
||||
|
||||
return (
|
||||
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
|
||||
<FlatList
|
||||
ListHeaderComponent = { renderListHeaderComponent }
|
||||
data = { answers }
|
||||
extraData = { answers }
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { answerListRef }
|
||||
renderItem = { renderListItem } />
|
||||
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.addOption'
|
||||
disabled = { answers.length >= ANSWERS_LIMIT }
|
||||
id = { t('polls.create.addOption') }
|
||||
labelKey = 'polls.create.addOption'
|
||||
onClick = { () => {
|
||||
// adding and answer
|
||||
addAnswer();
|
||||
requestFocus(answers.length);
|
||||
} }
|
||||
style = { pollsStyles.pollCreateAddButton }
|
||||
type = { SECONDARY } />
|
||||
<View
|
||||
style = { pollsStyles.buttonRow as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.cancel'
|
||||
id = { t('polls.create.cancel') }
|
||||
labelKey = 'polls.create.cancel'
|
||||
onClick = { () => {
|
||||
setCreateMode(false);
|
||||
editingPollId
|
||||
&& editingPoll?.editing
|
||||
&& dispatch(editPoll(editingPollId, false));
|
||||
} }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { SECONDARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.save'
|
||||
disabled = { isSubmitDisabled }
|
||||
id = { t('polls.create.save') }
|
||||
labelKey = 'polls.create.save'
|
||||
onClick = { onSubmit }
|
||||
style = { pollsStyles.pollCreateButton }
|
||||
type = { PRIMARY } />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollCreate to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollCreate(PollCreate);
|
||||
45
react/features/polls/components/native/PollItem.tsx
Normal file
45
react/features/polls/components/native/PollItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { shouldShowResults } from '../../functions';
|
||||
|
||||
import PollAnswer from './PollAnswer';
|
||||
import PollResults from './PollResults';
|
||||
import { pollsStyles } from './styles';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Id of the poll.
|
||||
*/
|
||||
pollId: string;
|
||||
|
||||
/**
|
||||
* Create mode control.
|
||||
*/
|
||||
setCreateMode: (mode: boolean) => void;
|
||||
|
||||
}
|
||||
|
||||
const PollItem = ({ pollId, setCreateMode }: IProps) => {
|
||||
const showResults = useSelector(shouldShowResults(pollId));
|
||||
|
||||
return (
|
||||
<View
|
||||
id = 'poll-item-container'
|
||||
style = { pollsStyles.pollItemContainer as ViewStyle }>
|
||||
{ showResults
|
||||
? <PollResults
|
||||
key = { pollId }
|
||||
pollId = { pollId } />
|
||||
: <PollAnswer
|
||||
pollId = { pollId }
|
||||
setCreateMode = { setCreateMode } />
|
||||
}
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollItem;
|
||||
149
react/features/polls/components/native/PollResults.tsx
Normal file
149
react/features/polls/components/native/PollResults.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import {
|
||||
default as AbstractPollResults,
|
||||
type AbstractProps,
|
||||
type AnswerInfo
|
||||
} from '../AbstractPollResults';
|
||||
|
||||
import { dialogStyles, pollsStyles, resultsStyles } from './styles';
|
||||
|
||||
/**
|
||||
* Component that renders the poll results.
|
||||
*
|
||||
* @param {Props} props - The passed props.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const PollResults = (props: AbstractProps) => {
|
||||
const {
|
||||
answers,
|
||||
changeVote,
|
||||
creatorName,
|
||||
haveVoted,
|
||||
question,
|
||||
showDetails,
|
||||
t,
|
||||
toggleIsDetailed
|
||||
} = props;
|
||||
|
||||
/**
|
||||
* Render a header summing up answer information.
|
||||
*
|
||||
* @param {string} answer - The name of the answer.
|
||||
* @param {number} percentage - The percentage of voters.
|
||||
* @param {number} nbVotes - The number of collected votes.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const renderHeader = (answer: string, percentage: number, nbVotes: number) => (
|
||||
<View style = { resultsStyles.answerHeader as ViewStyle }>
|
||||
<Text style = { resultsStyles.answer as TextStyle }>{ answer }</Text>
|
||||
<View>
|
||||
<Text style = { resultsStyles.answer as TextStyle }>({nbVotes}) {percentage}%</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render voters of and answer.
|
||||
*
|
||||
* @param {AnswerInfo} answer - The answer info.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const renderRow = useCallback((answer: AnswerInfo) => {
|
||||
const { name, percentage, voters, voterCount } = answer;
|
||||
|
||||
if (showDetails) {
|
||||
return (
|
||||
<View style = { resultsStyles.answerContainer as ViewStyle }>
|
||||
{ renderHeader(name, percentage, voterCount) }
|
||||
<View style = { resultsStyles.barContainer as ViewStyle }>
|
||||
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] as ViewStyle[] } />
|
||||
</View>
|
||||
{ voters && voterCount > 0
|
||||
&& <View style = { resultsStyles.voters as ViewStyle }>
|
||||
{/* @ts-ignore */}
|
||||
{voters.map(({ id, name: voterName }) =>
|
||||
(<Text
|
||||
key = { id }
|
||||
style = { resultsStyles.voter as TextStyle }>
|
||||
{ voterName }
|
||||
</Text>)
|
||||
)}
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// else, we display a simple list
|
||||
// We add a progress bar by creating an empty view of width equal to percentage.
|
||||
return (
|
||||
<View style = { resultsStyles.answerContainer as ViewStyle }>
|
||||
{ renderHeader(answer.name, percentage, voterCount) }
|
||||
<View style = { resultsStyles.barContainer as ViewStyle }>
|
||||
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] as ViewStyle[] } />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
}, [ showDetails ]);
|
||||
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<View>
|
||||
<Text
|
||||
id = 'question-text'
|
||||
style = { dialogStyles.questionText as TextStyle } >{ question }</Text>
|
||||
<Text
|
||||
id = 'poll-owner-text'
|
||||
style = { dialogStyles.questionOwnerText as TextStyle } >
|
||||
{ t('polls.by', { name: creatorName }) }
|
||||
</Text>
|
||||
<FlatList
|
||||
data = { answers }
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
renderItem = { answer => renderRow(answer.item) } />
|
||||
<View style = { pollsStyles.bottomLinks as ViewStyle }>
|
||||
<Button
|
||||
id = {
|
||||
showDetails
|
||||
? t('polls.results.hideDetailedResults')
|
||||
: t('polls.results.showDetailedResults')
|
||||
}
|
||||
labelKey = {
|
||||
showDetails
|
||||
? 'polls.results.hideDetailedResults'
|
||||
: 'polls.results.showDetailedResults'
|
||||
}
|
||||
labelStyle = { pollsStyles.toggleText }
|
||||
onClick = { toggleIsDetailed }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
<Button
|
||||
id = {
|
||||
haveVoted
|
||||
? t('polls.results.changeVote')
|
||||
: t('polls.results.vote')
|
||||
}
|
||||
labelKey = {
|
||||
haveVoted
|
||||
? 'polls.results.changeVote'
|
||||
: 'polls.results.vote'
|
||||
}
|
||||
labelStyle = { pollsStyles.toggleText }
|
||||
onClick = { changeVote }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollResults to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults);
|
||||
70
react/features/polls/components/native/PollsList.tsx
Normal file
70
react/features/polls/components/native/PollsList.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconMessage } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
import PollItem from './PollItem';
|
||||
import { pollsStyles } from './styles';
|
||||
|
||||
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);
|
||||
|
||||
const renderItem = useCallback(({ item }) => (
|
||||
<PollItem
|
||||
key = { item }
|
||||
pollId = { item }
|
||||
setCreateMode = { setCreateMode } />)
|
||||
, []);
|
||||
|
||||
const flatlistRef = useRef<FlatList>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
flatlistRef.current?.scrollToEnd({ animated: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [ polls ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
listPolls.length === 0
|
||||
&& <View style = { pollsStyles.noPollContent as ViewStyle }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon03 }
|
||||
size = { 160 }
|
||||
src = { IconMessage } />
|
||||
<Text
|
||||
id = 'no-polls-text'
|
||||
style = { pollsStyles.noPollText as TextStyle } >
|
||||
{
|
||||
t('polls.results.empty')
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
<FlatList
|
||||
data = { listPolls }
|
||||
extraData = { listPolls }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { flatlistRef }
|
||||
renderItem = { renderItem } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollsList;
|
||||
77
react/features/polls/components/native/PollsPane.tsx
Normal file
77
react/features/polls/components/native/PollsPane.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { StyleType } from '../../../base/styles/functions.any';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { ChatTabs } from '../../../chat/constants';
|
||||
import { TabBarLabelCounter }
|
||||
from '../../../mobile/navigation/components/TabBarLabelCounter';
|
||||
import {
|
||||
default as AbstractPollsPane,
|
||||
type AbstractProps
|
||||
} from '../AbstractPollsPane';
|
||||
|
||||
import PollCreate from './PollCreate';
|
||||
import PollsList from './PollsList';
|
||||
import { pollsStyles } from './styles';
|
||||
|
||||
const PollsPane = (props: AbstractProps) => {
|
||||
const { createMode, isCreatePollsDisabled, onCreate, setCreateMode, t } = props;
|
||||
const navigation = useNavigation();
|
||||
const isPollsTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.POLLS);
|
||||
const { nbUnreadPolls } = useSelector((state: IReduxState) => state['features/polls']);
|
||||
|
||||
useEffect(() => {
|
||||
const activeUnreadPollsNr = !isPollsTabFocused && nbUnreadPolls > 0;
|
||||
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
tabBarLabel: () => (
|
||||
<TabBarLabelCounter
|
||||
activeUnreadNr = { activeUnreadPollsNr }
|
||||
isFocused = { isPollsTabFocused }
|
||||
label = { t('chat.tabs.polls') }
|
||||
nbUnread = { nbUnreadPolls } />
|
||||
)
|
||||
});
|
||||
|
||||
}, [ isPollsTabFocused, nbUnreadPolls ]);
|
||||
|
||||
const createPollButtonStyles = Platform.OS === 'android'
|
||||
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
contentContainerStyle = { pollsStyles.pollPane as StyleType }
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
{
|
||||
createMode
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <>
|
||||
<PollsList setCreateMode = { setCreateMode } />
|
||||
{!isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
id = { t('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.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollsPane(PollsPane);
|
||||
239
react/features/polls/components/native/styles.ts
Normal file
239
react/features/polls/components/native/styles.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { createStyleSheet } from '../../../base/styles/functions.native';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
|
||||
export const dialogStyles = createStyleSheet({
|
||||
|
||||
headerContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
customContainer: {
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
questionText: {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginLeft: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
questionOwnerText: {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
color: BaseTheme.palette.text03,
|
||||
marginBottom: BaseTheme.spacing[2],
|
||||
marginLeft: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
optionContainer: {
|
||||
flexDirection: 'column',
|
||||
marginTop: BaseTheme.spacing[3],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
optionRemoveButton: {
|
||||
width: 128
|
||||
},
|
||||
|
||||
optionRemoveButtonText: {
|
||||
color: BaseTheme.palette.link01
|
||||
},
|
||||
|
||||
field: {
|
||||
borderWidth: 1,
|
||||
borderColor: BaseTheme.palette.ui06,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 14,
|
||||
paddingBottom: BaseTheme.spacing[2],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
paddingTop: BaseTheme.spacing[2]
|
||||
}
|
||||
});
|
||||
|
||||
export const resultsStyles = createStyleSheet({
|
||||
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
|
||||
barContainer: {
|
||||
backgroundColor: '#ccc',
|
||||
borderRadius: 3,
|
||||
width: '100%',
|
||||
height: 6,
|
||||
marginTop: 2
|
||||
},
|
||||
|
||||
bar: {
|
||||
backgroundColor: BaseTheme.palette.action01,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: 6
|
||||
},
|
||||
|
||||
voters: {
|
||||
backgroundColor: BaseTheme.palette.ui04,
|
||||
borderColor: BaseTheme.palette.ui03,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
borderWidth: 1,
|
||||
padding: BaseTheme.spacing[2],
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
voter: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
answerContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[1],
|
||||
marginVertical: BaseTheme.spacing[3],
|
||||
maxWidth: '100%'
|
||||
},
|
||||
|
||||
answerHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
answer: {
|
||||
color: BaseTheme.palette.text01,
|
||||
flexShrink: 1
|
||||
},
|
||||
|
||||
answerVoteCount: {
|
||||
paddingLeft: 10
|
||||
},
|
||||
|
||||
chatQuestion: {
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
});
|
||||
|
||||
export const pollsStyles = createStyleSheet({
|
||||
|
||||
noPollContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '25%'
|
||||
},
|
||||
|
||||
noPollText: {
|
||||
flex: 1,
|
||||
color: BaseTheme.palette.text03,
|
||||
textAlign: 'center',
|
||||
maxWidth: '70%'
|
||||
},
|
||||
|
||||
pollItemContainer: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderColor: BaseTheme.palette.ui06,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
boxShadow: BaseTheme.shape.boxShadow,
|
||||
borderWidth: 1,
|
||||
padding: BaseTheme.spacing[2],
|
||||
margin: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateContainer: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
pollCreateSubContainer: {
|
||||
flex: 1,
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerAndroid: {
|
||||
marginBottom: BaseTheme.spacing[8],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerIos: {
|
||||
marginBottom: BaseTheme.spacing[5],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollSendLabel: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
pollSendDisabledLabel: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
answerContent: {
|
||||
marginBottom: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
switchRow: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
padding: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
switchLabel: {
|
||||
color: BaseTheme.palette.text01,
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
pollCreateAddButton: {
|
||||
marginHorizontal: BaseTheme.spacing[1],
|
||||
marginVertical: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
pollCreateButton: {
|
||||
marginHorizontal: BaseTheme.spacing[1],
|
||||
flex: 1
|
||||
},
|
||||
|
||||
toggleText: {
|
||||
color: BaseTheme.palette.action01
|
||||
},
|
||||
|
||||
createPollButtonIos: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
createPollButtonAndroid: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
pollPane: {
|
||||
flex: 1,
|
||||
padding: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
pollPaneContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
bottomLinks: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginHorizontal: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
fieldSeparator: {
|
||||
borderBottomWidth: 1,
|
||||
borderColor: BaseTheme.palette.ui05,
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user