feat(polls) Redesign (#12838)

Convert files to TS
Move styles from SCSS to JSS
Implement redesign
This commit is contained in:
Robert Pintilii 2023-01-30 11:35:21 +02:00 committed by GitHub
parent 921f3ee8cd
commit 4f34a576d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 478 additions and 655 deletions

View File

@ -1,353 +1,3 @@
.poll-dialog {
font-size: 14px;
font-weight: 400;
line-height: 20px;
h1, span, li, strong {
color: #bce;
}
ol {
margin: 0;
}
}
.poll-question-field {
padding: 8px 16px;
padding-bottom: 24px;
border-bottom: 1px solid #525252;
}
.poll-header {
margin-bottom: 8px;
}
.poll-creator {
color: #C2C2C2;
font-weight: 600;
margin: 4px 0 16px 0;
}
.poll-answer-container {
display: flex;
padding: 4px;
background: #3D3D3D;
border-radius: 3px;
margin-bottom: 8px;
@media (max-width: 580px) {
&> span {
padding: 8px 0;
}
svg {
margin-top: 6px;
}
}
}
.poll-answer-field-list, .poll-answer-list, .poll-result-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.poll-answer-field-list {
padding: 0 16px;
}
ol.poll-result-list {
margin-bottom: 1.5em;
}
.poll-result-list > li {
margin-bottom: 16px;
}
.poll-answer-field {
flex-direction: column;
align-items: stretch;
margin-bottom: 16;
}
.poll-answer-field:last-child {
margin-bottom: 0;
}
.poll-create-option-row {
display: flex;
margin-bottom: 4;
}
// Needed to override atlaskit default blue color
.poll-create-container .jsYMHu {
background: #292929;
border-color: #808090;
color: #fff // #808090
}
.poll-add-button {
display: flex;
justify-content: center;
padding: 8px 16px;
}
.poll-remove-option-button {
background: 0 0;
border: none;
color: #E04757;
padding-left: 0;
}
.poll-create-add-option {
border: none;
background-color: #292929;
padding: 3px;
width: 100%;
}
.poll-icon-button, .poll-drag-handle {
.jitsi-icon svg {
fill: #929292;
}
}
.poll-drag-handle {
background-color: transparent;
border: none;
cursor: grab;
padding-left: 8;
padding-top: 8px;
display: flex;
}
.poll-question {
font-size: 16px;
font-weight: 600;
line-height: 26px;
}
.poll-answer-voters {
font-weight: lighter;
list-style-type: none;
border: #616161 solid 1px;
border-radius: 3px;
padding: 2px 6px;
margin: 4px 0px 12px;
background-color: #616161;
}
.poll-answer-header {
display: flex;
justify-content: space-between;
}
.poll-answer-vote-name {
flex-shrink: 1;
overflow-wrap: anywhere
}
.poll-answer-vote-count-container{
display: flex;
}
.poll-answer-vote-count {
margin-left: 10px;
white-space: nowrap;
flex: 1;
text-align: right;
}
.poll-answer-short-results{
display: flex;
min-width: 10em;
justify-content: space-between;
align-items: center;
}
.poll-bar-container, .poll-bar {
border-radius: 3px;
height: 6px;
}
.poll-bar-container {
background-color: #616161;
max-width: 160px;
margin-top: 3px;
flex: 1;
}
.poll-bar {
background-color: #246FE5;
}
.poll-message-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-top: 5px;
}
.poll-notice {
font-weight: 100;
margin-right: 10px;
}
.poll-show-details {
background-color: transparent;
border: none;
&:hover {
text-decoration: underline;
}
}
.poll-result-links {
display: flex;
flex-direction: row;
justify-content: space-between;
a.poll-detail-link, a.poll-change-vote-link {
color: #669AEC;
cursor: pointer;
font-weight: 600;
text-decoration: none;
&:hover {
color: #669AEC;
}
&:visited {
color: #669AEC;
}
}
}
.polls-pane-content {
height: 100%;
position: relative;
}
.pane-content{
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
width: 100%;
}
.empty-pane-icon {
width: 50%;
padding: 24px;
}
.empty-pane-icon svg {
fill: #3D3D3D;
width: 100%;
height: auto;
}
.empty-pane-message {
color: #fff;
padding: 0 16px;
text-align: center;
}
.poll-results, .poll-answer {
background: #292929;
border-radius: 8px;
border: 1px solid #666666;
margin: 16px;
padding: 16px;
word-break: break-word;
}
.poll-results {
color: #fff;
}
.poll-answer {
h1, strong ,span {
color: #fff;
}
button > span {
color: inherit;
}
}
.poll-create-label {
color: #C2C2C2;
display: flex;
font-weight: 400;
margin-bottom: 4;
}
.expandable-input{
line-height: 18px;
resize: none;
width: 100%;
height: 40px;
box-sizing: border-box;
overflow: hidden;
border: 1px solid #666666;
background-color: #141414;
color: #FFF;
border-radius: 6px;
padding: 10px 16px;
}
#polls-panel {
height: calc(100% - 119px);
}
.poll-container {
font-size: 14px;
font-weight: 600;
height: calc(100% - 88px);
line-height: 20px;
overflow-y: auto;
position: relative;
& > * + *:not(.ignore-child) {
margin-top: 16px;
}
@media (max-width: 580px) {
height: calc(100% - 102px);
}
}
.poll-create-header {
color: #fff;
font-size: 20px;
line-height: 28px;
margin: 20px 16px;
font-weight: 600;
}
.poll-create-container {
padding: 8px 0;
}
.poll-create-footer {
background-color: #141414;
bottom: 0;
position: absolute;
width: calc(100% - 32px);
}
.poll-footer {
display: flex;
justify-content: space-between;
padding: 0 16px 16px 16px;
}
.poll-answer-footer {
padding: 8px 0 0 0;
}

View File

@ -85,6 +85,7 @@ export interface IJitsiConference {
sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
sendFeedback: Function;
sendLobbyMessage: Function;
sendMessage: Function;
sessionId: string;
setDesktopSharingFrameRate: Function;
setDisplayName: Function;

View File

@ -21,6 +21,7 @@ interface IProps extends IInputProps {
name?: string;
onKeyPress?: (e: React.KeyboardEvent) => void;
readOnly?: boolean;
required?: boolean;
textarea?: boolean;
type?: 'text' | 'email' | 'number' | 'password';
}
@ -148,6 +149,7 @@ const Input = React.forwardRef<any, IProps>(({
onKeyPress,
placeholder,
readOnly = false,
required,
textarea = false,
type = 'text',
value
@ -178,6 +180,7 @@ const Input = React.forwardRef<any, IProps>(({
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
disabled = { disabled }
{ ...(id ? { id } : {}) }
maxLength = { maxLength }
maxRows = { maxRows }
minRows = { minRows }
name = { name }
@ -186,6 +189,7 @@ const Input = React.forwardRef<any, IProps>(({
placeholder = { placeholder }
readOnly = { readOnly }
ref = { ref }
required = { required }
value = { value } />
) : (
<input
@ -202,6 +206,7 @@ const Input = React.forwardRef<any, IProps>(({
placeholder = { placeholder }
readOnly = { readOnly }
ref = { ref }
required = { required }
type = { type }
value = { value } />
)}

View File

@ -1,18 +1,17 @@
// @flow
import React, { useCallback, useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { createPollEvent, sendAnalytics } from '../../analytics';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { COMMAND_NEW_POLL } from '../constants';
/**
* The type of the React {@code Component} props of inheriting component.
*/
type InputProps = {
setCreateMode: boolean => void,
setCreateMode: (mode: boolean) => void;
};
/*
@ -20,16 +19,15 @@ type InputProps = {
* concrete implementations (web/native).
**/
export type AbstractProps = InputProps & {
answers: Array<string>,
question: string,
setQuestion: string => void,
setAnswer: (number, string) => void,
addAnswer: ?number => void,
moveAnswer: (number, number) => void,
removeAnswer: number => void,
onSubmit: Function,
isSubmitDisabled: boolean,
t: Function,
addAnswer: (index?: number) => void;
answers: Array<string>;
isSubmitDisabled: boolean;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
question: string;
removeAnswer: (index: number) => void;
setAnswer: (index: number, value: string) => void;
setQuestion: (question: string) => void;
t: Function;
};
/**
@ -39,7 +37,7 @@ export type AbstractProps = InputProps & {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { setCreateMode } = props;
@ -51,25 +49,15 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
answers[i] = answer;
setAnswers([ ...answers ]);
});
}, [ answers ]);
const addAnswer = useCallback((i: ?number) => {
const addAnswer = useCallback((i?: number) => {
const newAnswers = [ ...answers ];
sendAnalytics(createPollEvent('option.added'));
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
setAnswers(newAnswers);
});
const moveAnswer = useCallback((i, j) => {
const newAnswers = [ ...answers ];
const answer = answers[i];
sendAnalytics(createPollEvent('option.moved'));
newAnswers.splice(i, 1);
newAnswers.splice(j, 0, answer);
setAnswers(newAnswers);
});
}, [ answers ]);
const removeAnswer = useCallback(i => {
if (answers.length <= 2) {
@ -80,9 +68,9 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
sendAnalytics(createPollEvent('option.removed'));
newAnswers.splice(i, 1);
setAnswers(newAnswers);
});
}, [ answers ]);
const conference = useSelector(state => state['features/base/conference'].conference);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const onSubmit = useCallback(ev => {
if (ev) {
@ -95,7 +83,7 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
return;
}
conference.sendMessage({
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
question,
@ -118,7 +106,6 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
addAnswer = { addAnswer }
answers = { answers }
isSubmitDisabled = { isSubmitDisabled }
moveAnswer = { moveAnswer }
onSubmit = { onSubmit }
question = { question }
removeAnswer = { removeAnswer }

View File

@ -1,13 +1,11 @@
// @flow
import React, { useCallback, useMemo, useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent, sendAnalytics } from '../../analytics';
import { getParticipantDisplayName } from '../../base/participants';
import { getParticipantById } from '../../base/participants/functions';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { getParticipantById, getParticipantDisplayName } from '../../base/participants/functions';
import { useBoundSelector } from '../../base/util/hooks';
import { setVoteChanging } from '../actions';
import { getPoll } from '../functions';
@ -20,28 +18,28 @@ type InputProps = {
/**
* ID of the poll to display.
*/
pollId: string,
pollId: string;
};
export type AnswerInfo = {
name: string,
percentage: number,
voters?: Array<{ id: number, name: string }>,
voterCount: number
name: string;
percentage: number;
voterCount: number;
voters?: Array<{ id: string; name: string; } | undefined>;
};
/**
* The type of the React {@code Component} props of {@link AbstractPollResults}.
*/
export type AbstractProps = {
answers: Array<AnswerInfo>,
changeVote: Function,
creatorName: string,
showDetails: boolean,
question: string,
t: Function,
toggleIsDetailed: Function,
haveVoted: boolean,
answers: Array<AnswerInfo>;
changeVote: (e: React.MouseEvent) => void;
creatorName: string;
haveVoted: boolean;
question: string;
showDetails: boolean;
t: Function;
toggleIsDetailed: (e: React.MouseEvent) => void;
};
/**
@ -51,18 +49,18 @@ export type AbstractProps = {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { pollId } = props;
const pollDetails = useSelector(getPoll(pollId));
const participant = useBoundSelector(getParticipantById, pollDetails.senderId);
const reduxState = useSelector(state => state);
const reduxState = useSelector((state: IReduxState) => state);
const [ showDetails, setShowDetails ] = useState(false);
const toggleIsDetailed = useCallback(() => {
sendAnalytics(createPollEvent('vote.detailsViewed'));
setShowDetails(!showDetails);
});
setShowDetails(details => !details);
}, []);
const answers: Array<AnswerInfo> = useMemo(() => {
const allVoters = new Set();
@ -79,7 +77,7 @@ const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (pr
const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0;
const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0;
let voters = null;
let voters;
if (showDetails && answer.voters) {
const answerVoters = answer.voters?.length ? [ ...answer.voters ] : Object.keys({ ...answer.voters });

View File

@ -1,7 +1,4 @@
// @flow
import React, { useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, useState } from 'react';
import { useTranslation } from 'react-i18next';
/*
@ -9,10 +6,10 @@ import { useTranslation } from 'react-i18next';
* concrete implementations (web/native).
**/
export type AbstractProps = {
createMode: boolean,
onCreate: void => void,
setCreateMode: boolean => void,
t: Function,
createMode: boolean;
onCreate: () => void;
setCreateMode: (mode: boolean) => void;
t: Function;
};
/**
@ -22,7 +19,7 @@ export type AbstractProps = {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollsPane = (Component: AbstractComponent<AbstractProps>) => () => {
const AbstractPollsPane = (Component: ComponentType<AbstractProps>) => () => {
const [ createMode, setCreateMode ] = useState(false);

View File

@ -1,7 +1,7 @@
import React from 'react';
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';
@ -10,8 +10,40 @@ import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
const useStyles = makeStyles()(theme => {
return {
container: {
margin: '24px',
padding: '16px',
backgroundColor: theme.palette.ui02,
borderRadius: '8px'
},
header: {
marginBottom: '24px'
},
question: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
marginBottom: '8px'
},
creator: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text02
},
answerList: {
listStyleType: 'none',
margin: 0,
padding: 0,
marginBottom: '24px'
},
answer: {
display: 'flex',
marginBottom: '16px'
},
footer: {
display: 'flex',
justifyContent: 'flex-end'
},
buttonMargin: {
marginRight: theme.spacing(2)
marginRight: theme.spacing(3)
}
};
});
@ -27,23 +59,23 @@ const PollAnswer = ({
t
}: AbstractProps) => {
const { changingVote } = poll;
const { classes: styles } = useStyles();
const { classes } = useStyles();
return (
<div className = 'poll-answer'>
<div className = 'poll-header'>
<div className = 'poll-question'>
<span>{ poll.question }</span>
<div className = { classes.container }>
<div className = { classes.header }>
<div className = { classes.question }>
{ poll.question }
</div>
<div className = 'poll-creator'>
<div className = { classes.creator }>
{ t('polls.by', { name: creatorName }) }
</div>
</div>
<ol className = 'poll-answer-list'>
<ul className = { classes.answerList }>
{
poll.answers.map((answer: any, index: number) => (
<li
className = 'poll-answer-container'
className = { classes.answer }
key = { index }>
<Checkbox
checked = { checkBoxStates[index] }
@ -54,19 +86,17 @@ const PollAnswer = ({
</li>
))
}
</ol>
<div className = 'poll-footer poll-answer-footer' >
</ul>
<div className = { classes.footer } >
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { styles.buttonMargin }
fullWidth = { true }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
fullWidth = { true }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
</div>

View File

@ -1,21 +1,62 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
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 { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
// @ts-ignore
import AbstractPollCreate from '../AbstractPollCreate';
// @ts-ignore
import type { AbstractProps } from '../AbstractPollCreate';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '100%',
position: 'relative'
},
createContainer: {
padding: '0 24px',
height: 'calc(100% - 88px)',
overflowY: 'auto'
},
header: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
margin: '24px 0 16px'
},
questionContainer: {
paddingBottom: '24px',
borderBottom: `1px solid ${theme.palette.ui03}`
},
answerList: {
listStyleType: 'none',
margin: 0,
padding: 0
},
answer: {
marginBottom: '24px'
},
removeOption: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.link01,
marginTop: '8px',
border: 0,
background: 'transparent'
},
addButtonContainer: {
display: 'flex'
},
footer: {
position: 'absolute',
bottom: 0,
display: 'flex',
justifyContent: 'flex-end',
padding: '24px',
width: '100%',
boxSizing: 'border-box'
},
buttonMargin: {
marginRight: theme.spacing(2)
marginRight: theme.spacing(3)
}
};
});
@ -32,7 +73,7 @@ const PollCreate = ({
setQuestion,
t
}: AbstractProps) => {
const { classes: styles } = useStyles();
const { classes } = useStyles();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@ -139,76 +180,54 @@ const PollCreate = ({
}
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
const autogrow = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
const el = ev.target;
el.style.height = '1px';
el.style.height = `${el.scrollHeight + 2}px`;
};
/* eslint-disable react/jsx-no-bind */
return (<form
className = 'polls-pane-content'
className = { classes.container }
onSubmit = { onSubmit }>
<div className = 'poll-create-container poll-container'>
<div className = 'poll-create-header'>
<div className = { classes.createContainer }>
<div className = { classes.header }>
{ t('polls.create.create') }
</div>
<div className = 'poll-question-field'>
<span className = 'poll-create-label'>
{ t('polls.create.pollQuestion') }
</span>
<textarea
<div className = { classes.questionContainer }>
<Input
autoFocus = { true }
className = 'expandable-input'
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { ev => setQuestion(ev.target.value) }
onInput = { autogrow }
onKeyDown = { onQuestionKeyDown }
onChange = { setQuestion }
onKeyPress = { onQuestionKeyDown }
placeholder = { t('polls.create.questionPlaceholder') }
required = { true }
rows = { 1 }
textarea = { true }
value = { question } />
</div>
<ol className = 'poll-answer-field-list'>
<ol className = { classes.answerList }>
{answers.map((answer: any, i: number) =>
(<li
className = 'poll-answer-field'
className = { classes.answer }
key = { i }>
<span className = 'poll-create-label'>
{ t('polls.create.pollOption', { index: i + 1 })}
</span>
<div className = 'poll-create-option-row'>
<textarea
className = 'expandable-input'
maxLength = { CHAR_LIMIT }
onChange = { ev => setAnswer(i, ev.target.value) }
onInput = { autogrow }
onKeyDown = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
required = { true }
rows = { 1 }
value = { answer } />
</div>
<Input
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
textarea = { true }
value = { answer } />
{ answers.length > 2
&& <Tooltip content = { t('polls.create.removeOption') }>
<button
className = 'poll-remove-option-button'
onClick = { () => removeAnswer(i) }
type = 'button'>
{ t('polls.create.removeOption') }
</button>
</Tooltip>}
&& <button
className = { classes.removeOption }
onClick = { () => removeAnswer(i) }
type = 'button'>
{ t('polls.create.removeOption') }
</button>}
</li>)
)}
</ol>
<div className = 'poll-add-button'>
<div className = { classes.addButtonContainer }>
<Button
accessibilityLabel = { t('polls.create.addOption') }
disabled = { answers.length >= ANSWERS_LIMIT }
fullWidth = { true }
labelKey = { 'polls.create.addOption' }
onClick = { () => {
addAnswer();
@ -217,23 +236,20 @@ const PollCreate = ({
type = { BUTTON_TYPES.SECONDARY } />
</div>
</div>
<div className = 'poll-footer poll-create-footer'>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.cancel') }
className = { styles.buttonMargin }
fullWidth = { true }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => setCreateMode(false) }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.send') }
disabled = { isSubmitDisabled }
fullWidth = { true }
isSubmit = { true }
labelKey = { 'polls.create.send' } />
</div>
</form>);
};
/*

View File

@ -1,22 +1,22 @@
// @flow
import React from 'react';
import { useSelector } from 'react-redux';
import { PollAnswer, PollResults } from '..';
import { shouldShowResults } from '../../functions';
import PollAnswer from './PollAnswer';
import PollResults from './PollResults';
type Props = {
interface IProps {
/**
* Id of the poll.
*/
pollId: string,
pollId: string;
}
const PollItem = React.forwardRef<Props, HTMLElement>(({ pollId }: Props, ref) => {
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
const showResults = useSelector(shouldShowResults(pollId));
return (

View File

@ -1,84 +0,0 @@
// @flow
import React from 'react';
import AbstractPollResults from '../AbstractPollResults';
import type { AbstractProps } from '../AbstractPollResults';
/**
* Component that renders the poll results.
*
* @param {Props} props - The passed props.
* @returns {React.Node}
*/
const PollResults = (props: AbstractProps) => {
const {
answers,
changeVote,
creatorName,
haveVoted,
showDetails,
question,
t,
toggleIsDetailed
} = props;
return (
<div className = 'poll-results'>
<div className = 'poll-header'>
<div className = 'poll-question'>
<strong>{ question }</strong>
</div>
<div className = 'poll-creator'>
{ t('polls.by', { name: creatorName }) }
</div>
</div>
<ol className = 'poll-result-list'>
{answers.map(({ name, percentage, voters, voterCount }, index) =>
(<li key = { index }>
<div className = 'poll-answer-header'>
<span className = 'poll-answer-vote-name' >{name}</span>
</div>
<div className = 'poll-answer-short-results'>
<span className = 'poll-bar-container'>
<div
className = 'poll-bar'
style = {{ width: `${percentage}%` }} />
</span>
<div className = 'poll-answer-vote-count-container'>
<span className = 'poll-answer-vote-count'>({voterCount}) {percentage}%</span>
</div>
</div>
{ showDetails && voters && voterCount > 0
&& <ul className = 'poll-answer-voters'>
{voters.map(voter =>
<li key = { voter.id }>{voter.name}</li>
)}
</ul>}
</li>)
)}
</ol>
<div className = { 'poll-result-links' }>
<a
className = { 'poll-detail-link' }
onClick = { toggleIsDetailed }>
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
</a>
<a
className = { 'poll-change-vote-link' }
onClick = { changeVote }>
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
</a>
</div>
</div>
);
};
/*
* 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);

View File

@ -0,0 +1,177 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import AbstractPollResults, { AbstractProps } from '../AbstractPollResults';
const useStyles = makeStyles()(theme => {
return {
container: {
margin: '24px',
padding: '16px',
backgroundColor: theme.palette.ui02,
borderRadius: '8px'
},
header: {
marginBottom: '16px'
},
question: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
marginBottom: '8px'
},
creator: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text02
},
resultList: {
listStyleType: 'none',
margin: 0,
padding: 0,
'& li': {
marginBottom: '16px'
}
},
answerName: {
display: 'flex',
flexShrink: 1,
overflowWrap: 'anywhere',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
marginBottom: '4px'
},
answerResultContainer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
minWidth: '10em'
},
barContainer: {
backgroundColor: theme.palette.ui03,
borderRadius: '4px',
height: '6px',
maxWidth: '160px',
width: '158px',
flexGrow: 1,
marginTop: '2px'
},
bar: {
height: '6px',
borderRadius: '4px',
backgroundColor: theme.palette.action01
},
voteCount: {
flex: 1,
textAlign: 'right',
...withPixelLineHeight(theme.typography.bodyShortBold),
color: theme.palette.text01
},
voters: {
margin: 0,
marginTop: '4px',
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.ui03,
borderRadius: theme.shape.borderRadius,
padding: '8px 16px',
'& li': {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
margin: 0,
marginBottom: '2px',
'&:last-of-type': {
marginBottom: 0
}
}
},
buttonsContainer: {
display: 'flex',
justifyContent: 'space-between',
'& button': {
border: 0,
backgroundColor: 'transparent',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.link01
}
}
};
});
/**
* Component that renders the poll results.
*
* @param {Props} props - The passed props.
* @returns {React.Node}
*/
const PollResults = ({
answers,
changeVote,
creatorName,
haveVoted,
showDetails,
question,
t,
toggleIsDetailed
}: AbstractProps) => {
const { classes } = useStyles();
return (
<div className = { classes.container }>
<div className = { classes.header }>
<div className = { classes.question }>
{question}
</div>
<div className = { classes.creator }>
{t('polls.by', { name: creatorName })}
</div>
</div>
<ul className = { classes.resultList }>
{answers.map(({ name, percentage, voters, voterCount }, index) =>
(<li key = { index }>
<div className = { classes.answerName }>
{name}
</div>
<div className = { classes.answerResultContainer }>
<span className = { classes.barContainer }>
<div
className = { classes.bar }
style = {{ width: `${percentage}%` }} />
</span>
<div className = { classes.voteCount }>
{voterCount}({percentage}%)
</div>
</div>
{showDetails && voters && voterCount > 0
&& <ul className = { classes.voters }>
{voters.map(voter =>
<li key = { voter?.id }>{voter?.name}</li>
)}
</ul>}
</li>)
)}
</ul>
<div className = { classes.buttonsContainer }>
<button
onClick = { toggleIsDetailed }>
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
</button>
<button
onClick = { changeVote }>
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
</button>
</div>
</div>
);
};
/*
* 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);

View File

@ -1,58 +0,0 @@
// @flow
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Icon, IconMessage } from '../../../base/icons';
import { browser } from '../../../base/lib-jitsi-meet';
import PollItem from './PollItem';
const PollsList = () => {
const { t } = useTranslation();
const polls = useSelector(state => state['features/polls'].polls);
const pollListEndRef = useRef(null);
const scrollToBottom = useCallback(() => {
if (pollListEndRef.current) {
// Safari does not support options
const param = browser.isSafari()
? false : {
behavior: 'smooth',
block: 'end',
inline: 'nearest'
};
pollListEndRef.current.scrollIntoView(param);
}
}, [ pollListEndRef.current ]);
useEffect(() => {
scrollToBottom();
}, [ polls ]);
const listPolls = Object.keys(polls);
return (
<>
{listPolls.length === 0
? <div className = 'pane-content'>
<Icon
className = 'empty-pane-icon'
src = { IconMessage } />
<span className = 'empty-pane-message'>{t('polls.results.empty')}</span>
</div>
: listPolls.map((id, index) => (
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
))}
</>
);
};
export default PollsList;

View File

@ -0,0 +1,89 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconMessage } from '../../../base/icons/svg';
import { browser } from '../../../base/lib-jitsi-meet';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import PollItem from './PollItem';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
},
emptyIcon: {
width: '100px',
padding: '16px',
'& svg': {
width: '100%',
height: 'auto'
}
},
emptyMessage: {
...withPixelLineHeight(theme.typography.bodyLongBold),
color: theme.palette.text02,
padding: '0 24px',
textAlign: 'center'
}
};
});
const PollsList = () => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
const pollListEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = useCallback(() => {
if (pollListEndRef.current) {
// Safari does not support options
const param = browser.isSafari()
? false : {
behavior: 'smooth' as const,
block: 'end' as const,
inline: 'nearest' as const
};
pollListEndRef.current.scrollIntoView(param);
}
}, [ pollListEndRef.current ]);
useEffect(() => {
scrollToBottom();
}, [ polls ]);
const listPolls = Object.keys(polls);
return (
<>
{listPolls.length === 0
? <div className = { classes.container }>
<Icon
className = { classes.emptyIcon }
color = { theme.palette.icon03 }
src = { IconMessage } />
<span className = { classes.emptyMessage }>{t('polls.results.empty')}</span>
</div>
: listPolls.map((id, index) => (
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
))}
</>
);
};
export default PollsList;

View File

@ -1,27 +1,42 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import Button from '../../../base/ui/components/web/Button';
// @ts-ignore
import AbstractPollsPane from '../AbstractPollsPane';
// @ts-ignore
import type { AbstractProps } from '../AbstractPollsPane';
import AbstractPollsPane, { AbstractProps } from '../AbstractPollsPane';
import PollCreate from './PollCreate';
// @ts-ignore
import PollsList from './PollsList';
const useStyles = makeStyles()(() => {
return {
container: {
height: '100%',
position: 'relative'
},
listContainer: {
height: 'calc(100% - 88px)',
overflowY: 'auto'
},
footer: {
position: 'absolute',
bottom: 0,
padding: '24px',
width: '100%',
boxSizing: 'border-box'
}
};
});
const PollsPane = (props: AbstractProps) => {
const { createMode, onCreate, setCreateMode, t } = props;
const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) => {
const { classes } = useStyles();
return createMode
? <PollCreate setCreateMode = { setCreateMode } />
: <div className = 'polls-pane-content'>
<div className = { 'poll-container' } >
: <div className = { classes.container }>
<div className = { classes.listContainer } >
<PollsList />
</div>
<div className = 'poll-footer poll-create-footer'>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.create') }
autoFocus = { true }