-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
427 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
import { gql, useQuery } from '@apollo/client' | ||
import { debounce } from 'lodash' | ||
import React from 'react' | ||
import { withNamespaces } from 'react-i18next' | ||
import { usePopper } from 'react-popper' | ||
import { connect } from 'react-redux' | ||
import styled, { css } from 'styled-components' | ||
|
||
import Statement from '../Statements/Statement' | ||
import Container from '../StyledUtils/Container' | ||
import { P } from '../StyledUtils/Text' | ||
import UnstyledButton from '../StyledUtils/UnstyledButton' | ||
import { Icon, LoadingFrame } from '../Utils' | ||
import Message from '../Utils/Message' | ||
import ActionBubbleMenu from './ActionBubbleMenu' | ||
|
||
const captionsQuery = gql` | ||
query VideoCaptionsQuery($videoId: ID!) { | ||
video(hashId: $videoId) { | ||
id | ||
captions { | ||
text | ||
start | ||
duration | ||
} | ||
} | ||
} | ||
` | ||
|
||
const CaptionText = styled.span` | ||
color: #000; | ||
transition: | ||
color 0.3s, | ||
text-shadow 0.3s; | ||
${({ $isCurrent, $isPlaying, $isPast }) => { | ||
if ($isPlaying) { | ||
if ($isCurrent) { | ||
return css` | ||
text-shadow: #9f9f9f 1px 1px 0px; | ||
color: #000; | ||
` | ||
} else if (!$isPast) { | ||
return css` | ||
color: #999; | ||
` | ||
} | ||
} | ||
}} | ||
` | ||
|
||
// A statement is displayed before each caption whe | ||
const getStatementsAtPosition = (statements, caption, nextCaption) => { | ||
const selected = [] | ||
for (const statement of statements) { | ||
if (statement.time >= caption.start) { | ||
if (!nextCaption || statement.time < nextCaption.start) { | ||
selected.push(statement) | ||
} else { | ||
break | ||
} | ||
} | ||
} | ||
|
||
return selected | ||
} | ||
|
||
const Arrow = styled.div` | ||
visibility: hidden; | ||
&, | ||
&::before { | ||
position: absolute; | ||
width: 8px; | ||
height: 8px; | ||
background: white; | ||
} | ||
&::before { | ||
visibility: visible; | ||
content: ''; | ||
transform: rotate(45deg); | ||
} | ||
` | ||
|
||
const StatementTooltip = styled.div` | ||
background: white; | ||
border: 1px solid #ccc; | ||
border-radius: 8px; | ||
padding: 8px; | ||
font-size: 12px; | ||
&[data-popper-placement^='top'] ${Arrow} { | ||
bottom: -4px; | ||
} | ||
&[data-popper-placement^='bottom'] ${Arrow} { | ||
top: -4px; | ||
} | ||
&[data-popper-placement^='left'] ${Arrow} { | ||
right: -4px; | ||
} | ||
&[data-popper-placement^='right'] ${Arrow} { | ||
left: -4px; | ||
} | ||
` | ||
|
||
const StatementIconButton = styled(UnstyledButton)` | ||
background: white; | ||
border: 1px solid #ccc; | ||
border-radius: 50%; | ||
padding: 4px; | ||
margin-right: 4px; | ||
cursor: pointer; | ||
width: 34px; | ||
height: 34px; | ||
&:hover { | ||
background: #f9f9f9; | ||
} | ||
` | ||
|
||
const StatementIndicator = ({ statement }) => { | ||
const [referenceElement, setReferenceElement] = React.useState(null) | ||
const [popperElement, setPopperElement] = React.useState(null) | ||
const [arrowElement, setArrowElement] = React.useState(null) | ||
const [show, setShow] = React.useState(false) | ||
const { styles, attributes } = usePopper(referenceElement, popperElement, { | ||
placement: 'top', | ||
modifiers: [ | ||
{ name: 'arrow', options: { element: arrowElement } }, | ||
{ name: 'offset', options: { offset: [0, 8] } }, | ||
{ name: 'flip', options: { fallbackPlacements: ['bottom'] } }, | ||
{ name: 'preventOverflow', options: { padding: 8 } }, | ||
], | ||
}) | ||
|
||
return ( | ||
<> | ||
<StatementIconButton | ||
ref={setReferenceElement} | ||
onFocus={() => setShow(true)} | ||
onBlur={() => setShow(false)} | ||
> | ||
<Icon name="commenting-o" size="small" className="has-text-primary" /> | ||
</StatementIconButton> | ||
|
||
{show && ( | ||
<StatementTooltip ref={setPopperElement} style={styles.popper} {...attributes.popper}> | ||
<Statement statement={statement} speaker={statement.speaker} withoutActions /> | ||
<Arrow ref={setArrowElement} style={styles.arrow} /> | ||
</StatementTooltip> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
const CaptionsExtractor = ({ t, videoId, playbackPosition, statements }) => { | ||
const { data, loading, error } = useQuery(captionsQuery, { variables: { videoId } }) | ||
const [selection, setSelection] = React.useState({ text: null }) | ||
const textContainerRef = React.useRef() | ||
|
||
// Watch for selection changes | ||
React.useEffect(() => { | ||
const watchForSelectionChange = debounce(() => { | ||
if (!textContainerRef.current) { | ||
return | ||
} | ||
|
||
const selection = document.getSelection() | ||
if (selection.rangeCount > 0) { | ||
const range = selection.getRangeAt(0) | ||
if (textContainerRef.current.contains(range.commonAncestorContainer)) { | ||
const firstNode = range.startContainer.parentNode | ||
setSelection({ | ||
text: selection.toString(), | ||
start: parseFloat(firstNode.getAttribute('data-start')) || 0, | ||
}) | ||
} | ||
} | ||
}, 100) | ||
|
||
document.addEventListener('selectionchange', watchForSelectionChange) | ||
return () => { | ||
document.removeEventListener('selectionchange', watchForSelectionChange) | ||
} | ||
}, []) | ||
|
||
if (loading) { | ||
return <LoadingFrame title={t('loading.captions')} /> | ||
} else if (error) { | ||
// eslint-disable-next-line no-console | ||
console.error(error) | ||
return <Message type="danger">{t('captions.errorLoading')}</Message> | ||
} else if (!data.video.captions?.length) { | ||
return <Message type="info">{t('captions.notFound')}</Message> | ||
} | ||
|
||
return ( | ||
<Container position="relative" id="ajfkdlsjfsd"> | ||
<P fontSize="20px" lineHeight="1.5" ref={textContainerRef}> | ||
{data.video.captions.map((caption, index) => { | ||
const nextCaption = data.video.captions[index + 1] | ||
const statementsAtPosition = getStatementsAtPosition(statements, caption, nextCaption) | ||
return ( | ||
<React.Fragment key={index}> | ||
{statementsAtPosition.map((statement) => ( | ||
<StatementIndicator key={statement.id} statement={statement} /> | ||
))} | ||
<CaptionText | ||
data-start={caption.start} | ||
$isPlaying={Boolean(playbackPosition)} | ||
$isPast={playbackPosition > caption.start + caption.duration} | ||
$isCurrent={ | ||
playbackPosition >= caption.start && | ||
playbackPosition <= caption.start + caption.duration | ||
} | ||
> | ||
{caption.text} | ||
</CaptionText>{' '} | ||
</React.Fragment> | ||
) | ||
})} | ||
</P> | ||
|
||
<ActionBubbleMenu | ||
hidden={!selection.text} | ||
playSelectionPosition={selection.start} | ||
getStatementInitialValues={() => { | ||
return { | ||
text: selection.text, | ||
time: Math.floor(selection.start), | ||
} | ||
}} | ||
/> | ||
</Container> | ||
) | ||
} | ||
|
||
export default connect((state) => ({ | ||
statements: state.VideoDebate.statements.data, | ||
playbackPosition: state.VideoDebate.video.playback.position, | ||
}))(withNamespaces('videoDebate')(CaptionsExtractor)) |
Oops, something went wrong.