diff --git a/css/_polls.scss b/css/_polls.scss index 7fc8634ccf..1ee4332691 100644 --- a/css/_polls.scss +++ b/css/_polls.scss @@ -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; -} diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index a16f6359ac..ba98f0a4db 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -85,6 +85,7 @@ export interface IJitsiConference { sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void; sendFeedback: Function; sendLobbyMessage: Function; + sendMessage: Function; sessionId: string; setDesktopSharingFrameRate: Function; setDisplayName: Function; diff --git a/react/features/base/ui/components/web/Input.tsx b/react/features/base/ui/components/web/Input.tsx index c6c6edc009..8d867484db 100644 --- a/react/features/base/ui/components/web/Input.tsx +++ b/react/features/base/ui/components/web/Input.tsx @@ -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(({ onKeyPress, placeholder, readOnly = false, + required, textarea = false, type = 'text', value @@ -178,6 +180,7 @@ const Input = React.forwardRef(({ 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(({ placeholder = { placeholder } readOnly = { readOnly } ref = { ref } + required = { required } value = { value } /> ) : ( (({ placeholder = { placeholder } readOnly = { readOnly } ref = { ref } + required = { required } type = { type } value = { value } /> )} diff --git a/react/features/polls/components/AbstractPollCreate.js b/react/features/polls/components/AbstractPollCreate.tsx similarity index 70% rename from react/features/polls/components/AbstractPollCreate.js rename to react/features/polls/components/AbstractPollCreate.tsx index 18d7d5872f..d59ce32475 100644 --- a/react/features/polls/components/AbstractPollCreate.js +++ b/react/features/polls/components/AbstractPollCreate.tsx @@ -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, - 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; + isSubmitDisabled: boolean; + onSubmit: (event?: FormEvent) => 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) => (props: InputProps) => { +const AbstractPollCreate = (Component: ComponentType) => (props: InputProps) => { const { setCreateMode } = props; @@ -51,25 +49,15 @@ const AbstractPollCreate = (Component: AbstractComponent) => (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) => (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) => (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) => (pro addAnswer = { addAnswer } answers = { answers } isSubmitDisabled = { isSubmitDisabled } - moveAnswer = { moveAnswer } onSubmit = { onSubmit } question = { question } removeAnswer = { removeAnswer } diff --git a/react/features/polls/components/AbstractPollResults.js b/react/features/polls/components/AbstractPollResults.tsx similarity index 76% rename from react/features/polls/components/AbstractPollResults.js rename to react/features/polls/components/AbstractPollResults.tsx index be9b39f018..49b4dee781 100644 --- a/react/features/polls/components/AbstractPollResults.js +++ b/react/features/polls/components/AbstractPollResults.tsx @@ -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, - changeVote: Function, - creatorName: string, - showDetails: boolean, - question: string, - t: Function, - toggleIsDetailed: Function, - haveVoted: boolean, + answers: Array; + 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) => (props: InputProps) => { +const AbstractPollResults = (Component: ComponentType) => (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 = useMemo(() => { const allVoters = new Set(); @@ -79,7 +77,7 @@ const AbstractPollResults = (Component: AbstractComponent) => (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 }); diff --git a/react/features/polls/components/AbstractPollsPane.js b/react/features/polls/components/AbstractPollsPane.tsx similarity index 75% rename from react/features/polls/components/AbstractPollsPane.js rename to react/features/polls/components/AbstractPollsPane.tsx index 818fe7a992..0ef274a069 100644 --- a/react/features/polls/components/AbstractPollsPane.js +++ b/react/features/polls/components/AbstractPollsPane.tsx @@ -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) => () => { +const AbstractPollsPane = (Component: ComponentType) => () => { const [ createMode, setCreateMode ] = useState(false); diff --git a/react/features/polls/components/web/PollAnswer.tsx b/react/features/polls/components/web/PollAnswer.tsx index 2ae0575cce..d95f40ca4c 100644 --- a/react/features/polls/components/web/PollAnswer.tsx +++ b/react/features/polls/components/web/PollAnswer.tsx @@ -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 ( -
-
-
- { poll.question } +
+
+
+ { poll.question }
-
+
{ t('polls.by', { name: creatorName }) }
-
    +
      { poll.answers.map((answer: any, index: number) => (
    • )) } -
-
+ +
diff --git a/react/features/polls/components/web/PollCreate.tsx b/react/features/polls/components/web/PollCreate.tsx index 5c3b12f6e1..84870af9c6 100644 --- a/react/features/polls/components/web/PollCreate.tsx +++ b/react/features/polls/components/web/PollCreate.tsx @@ -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) => { - const el = ev.target; - - el.style.height = '1px'; - el.style.height = `${el.scrollHeight + 2}px`; - }; - /* eslint-disable react/jsx-no-bind */ return (
-
-
+
+
{ t('polls.create.create') }
-
- - { t('polls.create.pollQuestion') } - -