feat(bubbles): ✨ Add video bubble
This commit is contained in:
@@ -267,3 +267,16 @@ export const ExternalLinkIcon = (props: IconProps) => (
|
|||||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const FilmIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
|
||||||
|
<line x1="7" y1="2" x2="7" y2="22"></line>
|
||||||
|
<line x1="17" y1="2" x2="17" y2="22"></line>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||||
|
<line x1="2" y1="7" x2="7" y2="7"></line>
|
||||||
|
<line x1="2" y1="17" x2="7" y2="17"></line>
|
||||||
|
<line x1="17" y1="17" x2="22" y2="17"></line>
|
||||||
|
<line x1="17" y1="7" x2="22" y2="7"></line>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
EditIcon,
|
EditIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
|
FilmIcon,
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
@@ -32,6 +33,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
|||||||
return <ChatIcon {...props} />
|
return <ChatIcon {...props} />
|
||||||
case BubbleStepType.IMAGE:
|
case BubbleStepType.IMAGE:
|
||||||
return <ImageIcon {...props} />
|
return <ImageIcon {...props} />
|
||||||
|
case BubbleStepType.VIDEO:
|
||||||
|
return <FilmIcon {...props} />
|
||||||
case InputStepType.TEXT:
|
case InputStepType.TEXT:
|
||||||
return <TextIcon {...props} />
|
return <TextIcon {...props} />
|
||||||
case InputStepType.NUMBER:
|
case InputStepType.NUMBER:
|
||||||
|
|||||||
@@ -13,54 +13,43 @@ type Props = { type: StepType }
|
|||||||
export const StepTypeLabel = ({ type }: Props) => {
|
export const StepTypeLabel = ({ type }: Props) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BubbleStepType.TEXT:
|
case BubbleStepType.TEXT:
|
||||||
case InputStepType.TEXT: {
|
case InputStepType.TEXT:
|
||||||
return <Text>Text</Text>
|
return <Text>Text</Text>
|
||||||
}
|
|
||||||
case BubbleStepType.IMAGE:
|
case BubbleStepType.IMAGE:
|
||||||
return <Text>Image</Text>
|
return <Text>Image</Text>
|
||||||
case InputStepType.NUMBER: {
|
case BubbleStepType.VIDEO:
|
||||||
|
return <Text>Video</Text>
|
||||||
|
case InputStepType.NUMBER:
|
||||||
return <Text>Number</Text>
|
return <Text>Number</Text>
|
||||||
}
|
case InputStepType.EMAIL:
|
||||||
case InputStepType.EMAIL: {
|
|
||||||
return <Text>Email</Text>
|
return <Text>Email</Text>
|
||||||
}
|
case InputStepType.URL:
|
||||||
case InputStepType.URL: {
|
|
||||||
return <Text>Website</Text>
|
return <Text>Website</Text>
|
||||||
}
|
case InputStepType.DATE:
|
||||||
case InputStepType.DATE: {
|
|
||||||
return <Text>Date</Text>
|
return <Text>Date</Text>
|
||||||
}
|
case InputStepType.PHONE:
|
||||||
case InputStepType.PHONE: {
|
|
||||||
return <Text>Phone</Text>
|
return <Text>Phone</Text>
|
||||||
}
|
case InputStepType.CHOICE:
|
||||||
case InputStepType.CHOICE: {
|
|
||||||
return <Text>Button</Text>
|
return <Text>Button</Text>
|
||||||
}
|
case LogicStepType.SET_VARIABLE:
|
||||||
case LogicStepType.SET_VARIABLE: {
|
|
||||||
return <Text>Set variable</Text>
|
return <Text>Set variable</Text>
|
||||||
}
|
case LogicStepType.CONDITION:
|
||||||
case LogicStepType.CONDITION: {
|
|
||||||
return <Text>Condition</Text>
|
return <Text>Condition</Text>
|
||||||
}
|
case LogicStepType.REDIRECT:
|
||||||
case LogicStepType.REDIRECT: {
|
|
||||||
return <Text>Redirect</Text>
|
return <Text>Redirect</Text>
|
||||||
}
|
case IntegrationStepType.GOOGLE_SHEETS:
|
||||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label="Google Sheets">
|
<Tooltip label="Google Sheets">
|
||||||
<Text>Sheets</Text>
|
<Text>Sheets</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label="Google Analytics">
|
<Tooltip label="Google Analytics">
|
||||||
<Text>Analytics</Text>
|
<Text>Analytics</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
default:
|
||||||
default: {
|
|
||||||
return <></>
|
return <></>
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import {
|
|||||||
PopoverArrow,
|
PopoverArrow,
|
||||||
PopoverBody,
|
PopoverBody,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ImagePopoverContent } from 'components/shared/ImageUploadContent'
|
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import {
|
import {
|
||||||
BubbleStep,
|
BubbleStep,
|
||||||
|
BubbleStepContent,
|
||||||
BubbleStepType,
|
BubbleStepType,
|
||||||
ImageBubbleContent,
|
|
||||||
ImageBubbleStep,
|
ImageBubbleStep,
|
||||||
TextBubbleStep,
|
TextBubbleStep,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
import { VideoUploadContent } from './VideoUploadContent'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: Exclude<BubbleStep, TextBubbleStep>
|
step: Exclude<BubbleStep, TextBubbleStep>
|
||||||
@@ -25,7 +26,7 @@ export const ContentPopover = ({ step }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<PopoverContent onMouseDown={handleMouseDown} w="500px">
|
<PopoverContent onMouseDown={handleMouseDown}>
|
||||||
<PopoverArrow />
|
<PopoverArrow />
|
||||||
<PopoverBody ref={ref} shadow="lg">
|
<PopoverBody ref={ref} shadow="lg">
|
||||||
<StepContent step={step} />
|
<StepContent step={step} />
|
||||||
@@ -37,16 +38,24 @@ export const ContentPopover = ({ step }: Props) => {
|
|||||||
|
|
||||||
export const StepContent = ({ step }: Props) => {
|
export const StepContent = ({ step }: Props) => {
|
||||||
const { updateStep } = useTypebot()
|
const { updateStep } = useTypebot()
|
||||||
const handleContentChange = (content: ImageBubbleContent) =>
|
|
||||||
|
const handleContentChange = (content: BubbleStepContent) =>
|
||||||
updateStep(step.id, { content } as Partial<ImageBubbleStep>)
|
updateStep(step.id, { content } as Partial<ImageBubbleStep>)
|
||||||
|
|
||||||
const handleNewImageSubmit = (url: string) => handleContentChange({ url })
|
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case BubbleStepType.IMAGE: {
|
case BubbleStepType.IMAGE: {
|
||||||
return (
|
return (
|
||||||
<ImagePopoverContent
|
<ImageUploadContent
|
||||||
url={step.content?.url}
|
content={step.content}
|
||||||
onSubmit={handleNewImageSubmit}
|
onSubmit={handleContentChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case BubbleStepType.VIDEO: {
|
||||||
|
return (
|
||||||
|
<VideoUploadContent
|
||||||
|
content={step.content}
|
||||||
|
onSubmit={handleContentChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Stack, Text } from '@chakra-ui/react'
|
||||||
|
import { InputWithVariableButton } from 'components/shared/InputWithVariableButton'
|
||||||
|
import { VideoBubbleContent, VideoBubbleContentType } from 'models'
|
||||||
|
import urlParser from 'js-video-url-parser/lib/base'
|
||||||
|
import 'js-video-url-parser/lib/provider/vimeo'
|
||||||
|
import 'js-video-url-parser/lib/provider/youtube'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
content?: VideoBubbleContent
|
||||||
|
onSubmit: (content: VideoBubbleContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||||
|
const handleUrlChange = (url: string) => {
|
||||||
|
const info = urlParser.parse(url)
|
||||||
|
return isDefined(info) && info.provider && info.id
|
||||||
|
? onSubmit({
|
||||||
|
type: info.provider as VideoBubbleContentType,
|
||||||
|
url,
|
||||||
|
id: info.id,
|
||||||
|
})
|
||||||
|
: onSubmit({ type: VideoBubbleContentType.URL, url })
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack p="2">
|
||||||
|
<InputWithVariableButton
|
||||||
|
placeholder="Paste the video link..."
|
||||||
|
initialValue={content?.url ?? ''}
|
||||||
|
onChange={handleUrlChange}
|
||||||
|
delay={100}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||||
|
Works with Youtube, Vimeo and others
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
SetVariableStep,
|
SetVariableStep,
|
||||||
ConditionStep,
|
ConditionStep,
|
||||||
IntegrationStepType,
|
IntegrationStepType,
|
||||||
|
VideoBubbleStep,
|
||||||
|
VideoBubbleContentType,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
||||||
import { SourceEndpoint } from './SourceEndpoint'
|
import { SourceEndpoint } from './SourceEndpoint'
|
||||||
@@ -48,6 +50,9 @@ export const StepNodeContent = ({ step }: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case BubbleStepType.VIDEO: {
|
||||||
|
return <VideoStepNodeContent step={step} />
|
||||||
|
}
|
||||||
case InputStepType.TEXT: {
|
case InputStepType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
@@ -182,3 +187,52 @@ const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VideoStepNodeContent = ({ step }: { step: VideoBubbleStep }) => {
|
||||||
|
if (!step.content?.url || !step.content.type)
|
||||||
|
return <Text color="gray.500">Click to edit...</Text>
|
||||||
|
switch (step.content.type) {
|
||||||
|
case VideoBubbleContentType.URL:
|
||||||
|
return (
|
||||||
|
<Box w="full" h="120px" pos="relative">
|
||||||
|
<video
|
||||||
|
key={step.content.url}
|
||||||
|
controls
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<source src={step.content.url} />
|
||||||
|
</video>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
case VideoBubbleContentType.VIMEO:
|
||||||
|
case VideoBubbleContentType.YOUTUBE: {
|
||||||
|
const baseUrl =
|
||||||
|
step.content.type === VideoBubbleContentType.VIMEO
|
||||||
|
? 'https://player.vimeo.com/video'
|
||||||
|
: 'https://www.youtube.com/embed'
|
||||||
|
return (
|
||||||
|
<Box w="full" h="120px" pos="relative">
|
||||||
|
<iframe
|
||||||
|
src={`${baseUrl}/${step.content.id}`}
|
||||||
|
allowFullScreen
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ import { SearchContextManager } from '@giphy/react-components'
|
|||||||
import { UploadButton } from '../buttons/UploadButton'
|
import { UploadButton } from '../buttons/UploadButton'
|
||||||
import { GiphySearch } from './GiphySearch'
|
import { GiphySearch } from './GiphySearch'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { ImageBubbleContent } from 'models'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
content?: ImageBubbleContent
|
||||||
onSubmit: (url: string) => void
|
onSubmit: (content: ImageBubbleContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageUploadContent = ({ url, onSubmit }: Props) => {
|
export const ImageUploadContent = ({ content, onSubmit }: Props) => {
|
||||||
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
|
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
|
||||||
'upload'
|
'upload'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleSubmit = (url: string) => onSubmit({ url })
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -43,7 +45,11 @@ export const ImageUploadContent = ({ url, onSubmit }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<BodyContent tab={currentTab} onSubmit={onSubmit} url={url} />
|
<BodyContent
|
||||||
|
tab={currentTab}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
url={content?.url}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { ImageUploadContent as ImagePopoverContent } from './ImageUploadContent'
|
export { ImageUploadContent } from './ImageUploadContent'
|
||||||
|
|||||||
116
apps/builder/cypress/tests/bubbles/video.ts
Normal file
116
apps/builder/cypress/tests/bubbles/video.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||||
|
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||||
|
import { getIframeBody } from 'cypress/support'
|
||||||
|
import { BubbleStepType, Step, VideoBubbleContentType } from 'models'
|
||||||
|
|
||||||
|
const videoSrc =
|
||||||
|
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
|
||||||
|
const youtubeVideoSrc = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||||
|
const vimeoVideoSrc = 'https://vimeo.com/649301125'
|
||||||
|
|
||||||
|
describe('Video bubbles', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('Content settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('seed')
|
||||||
|
createTypebotWithStep({
|
||||||
|
type: BubbleStepType.VIDEO,
|
||||||
|
} as Omit<Step, 'id' | 'blockId'>)
|
||||||
|
cy.signOut()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('upload image file correctly', () => {
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot3/edit')
|
||||||
|
cy.findByText('Click to edit...').click()
|
||||||
|
cy.findByPlaceholderText('Paste the video link...').type(videoSrc, {
|
||||||
|
waitForAnimations: false,
|
||||||
|
})
|
||||||
|
cy.get('video > source').should('have.attr', 'src').should('eq', videoSrc)
|
||||||
|
|
||||||
|
cy.findByPlaceholderText('Paste the video link...')
|
||||||
|
.clear()
|
||||||
|
.type(youtubeVideoSrc, {
|
||||||
|
waitForAnimations: false,
|
||||||
|
})
|
||||||
|
cy.get('iframe')
|
||||||
|
.should('have.attr', 'src')
|
||||||
|
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||||
|
|
||||||
|
cy.findByPlaceholderText('Paste the video link...')
|
||||||
|
.clear()
|
||||||
|
.type(vimeoVideoSrc, {
|
||||||
|
waitForAnimations: false,
|
||||||
|
})
|
||||||
|
cy.get('iframe')
|
||||||
|
.should('have.attr', 'src')
|
||||||
|
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Preview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('seed')
|
||||||
|
cy.signOut()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display video correctly', () => {
|
||||||
|
createTypebotWithStep({
|
||||||
|
type: BubbleStepType.VIDEO,
|
||||||
|
content: {
|
||||||
|
type: VideoBubbleContentType.URL,
|
||||||
|
url: videoSrc,
|
||||||
|
},
|
||||||
|
} as Omit<Step, 'id' | 'blockId'>)
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot3/edit')
|
||||||
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
|
getIframeBody()
|
||||||
|
.get('video > source')
|
||||||
|
.should('have.attr', 'src')
|
||||||
|
.should('eq', videoSrc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display youtube iframe correctly', () => {
|
||||||
|
createTypebotWithStep({
|
||||||
|
type: BubbleStepType.VIDEO,
|
||||||
|
content: {
|
||||||
|
type: VideoBubbleContentType.YOUTUBE,
|
||||||
|
url: youtubeVideoSrc,
|
||||||
|
id: 'dQw4w9WgXcQ',
|
||||||
|
},
|
||||||
|
} as Omit<Step, 'id' | 'blockId'>)
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot3/edit')
|
||||||
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
|
getIframeBody()
|
||||||
|
.get('iframe')
|
||||||
|
.first()
|
||||||
|
.should('have.attr', 'src')
|
||||||
|
.should('eq', 'https://www.youtube.com/embed/dQw4w9WgXcQ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display vimeo iframe correctly', () => {
|
||||||
|
createTypebotWithStep({
|
||||||
|
type: BubbleStepType.VIDEO,
|
||||||
|
content: {
|
||||||
|
type: VideoBubbleContentType.VIMEO,
|
||||||
|
url: vimeoVideoSrc,
|
||||||
|
id: '649301125',
|
||||||
|
},
|
||||||
|
} as Omit<Step, 'id' | 'blockId'>)
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot3/edit')
|
||||||
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
|
getIframeBody()
|
||||||
|
.get('iframe')
|
||||||
|
.first()
|
||||||
|
.should('have.attr', 'src')
|
||||||
|
.should('eq', 'https://player.vimeo.com/video/649301125')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"google-spreadsheet": "^3.2.0",
|
"google-spreadsheet": "^3.2.0",
|
||||||
"htmlparser2": "^7.2.0",
|
"htmlparser2": "^7.2.0",
|
||||||
"immer": "^9.0.7",
|
"immer": "^9.0.7",
|
||||||
|
"js-video-url-parser": "^0.5.1",
|
||||||
"kbar": "^0.1.0-beta.24",
|
"kbar": "^0.1.0-beta.24",
|
||||||
"micro": "^9.3.4",
|
"micro": "^9.3.4",
|
||||||
"micro-cors": "^0.1.1",
|
"micro-cors": "^0.1.1",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BubbleStep, BubbleStepType } from 'models'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ImageBubble } from './ImageBubble'
|
import { ImageBubble } from './ImageBubble'
|
||||||
import { TextBubble } from './TextBubble'
|
import { TextBubble } from './TextBubble'
|
||||||
|
import { VideoBubble } from './VideoBubble'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: BubbleStep
|
step: BubbleStep
|
||||||
@@ -14,5 +15,7 @@ export const HostBubble = ({ step, onTransitionEnd }: Props) => {
|
|||||||
return <TextBubble step={step} onTransitionEnd={onTransitionEnd} />
|
return <TextBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||||
case BubbleStepType.IMAGE:
|
case BubbleStepType.IMAGE:
|
||||||
return <ImageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
return <ImageBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||||
|
case BubbleStepType.VIDEO:
|
||||||
|
return <VideoBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Variable,
|
||||||
|
VideoBubbleContent,
|
||||||
|
VideoBubbleContentType,
|
||||||
|
VideoBubbleStep,
|
||||||
|
} from 'models'
|
||||||
|
import { TypingContent } from './TypingContent'
|
||||||
|
import { parseVariables } from 'services/variable'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
step: VideoBubbleStep
|
||||||
|
onTransitionEnd: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showAnimationDuration = 400
|
||||||
|
|
||||||
|
export const mediaLoadingFallbackTimeout = 5000
|
||||||
|
|
||||||
|
export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const { updateLastAvatarOffset } = useHostAvatars()
|
||||||
|
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [isTyping, setIsTyping] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showContentAfterMediaLoad()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const showContentAfterMediaLoad = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTyping(false)
|
||||||
|
onTypingEnd()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTypingEnd = () => {
|
||||||
|
setIsTyping(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
sendAvatarOffset()
|
||||||
|
onTransitionEnd()
|
||||||
|
}, showAnimationDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendAvatarOffset = () => {
|
||||||
|
if (!messageContainer.current) return
|
||||||
|
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
||||||
|
updateLastAvatarOffset(containerDimensions.top + containerDimensions.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col" ref={messageContainer}>
|
||||||
|
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||||
|
<div className={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||||
|
<div
|
||||||
|
className="flex items-center absolute px-4 py-2 rounded-lg bubble-typing z-10 "
|
||||||
|
style={{
|
||||||
|
width: isTyping ? '4rem' : '100%',
|
||||||
|
height: isTyping ? '2rem' : '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTyping ? <TypingContent /> : <></>}
|
||||||
|
</div>
|
||||||
|
<VideoContent
|
||||||
|
content={step.content}
|
||||||
|
isTyping={isTyping}
|
||||||
|
variables={typebot.variables}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoContent = ({
|
||||||
|
content,
|
||||||
|
isTyping,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
content?: VideoBubbleContent
|
||||||
|
isTyping: boolean
|
||||||
|
variables: Table<Variable>
|
||||||
|
}) => {
|
||||||
|
const url = useMemo(
|
||||||
|
() => parseVariables({ text: content?.url, variables: variables }),
|
||||||
|
[variables]
|
||||||
|
)
|
||||||
|
if (!content?.type) return <></>
|
||||||
|
switch (content.type) {
|
||||||
|
case VideoBubbleContentType.URL:
|
||||||
|
const isSafariBrowser = window.navigator.vendor.match(/apple/i)
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
className={
|
||||||
|
'p-4 focus:outline-none w-full z-10 content-opacity rounded-md ' +
|
||||||
|
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
height: isTyping ? '2rem' : 'auto',
|
||||||
|
maxHeight: isSafariBrowser ? '40vh' : '',
|
||||||
|
}}
|
||||||
|
autoPlay
|
||||||
|
>
|
||||||
|
<source src={url} type="video/mp4" />
|
||||||
|
Sorry, your browser doesn't support embedded videos.
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
case VideoBubbleContentType.VIMEO:
|
||||||
|
case VideoBubbleContentType.YOUTUBE: {
|
||||||
|
const baseUrl =
|
||||||
|
content.type === VideoBubbleContentType.VIMEO
|
||||||
|
? 'https://player.vimeo.com/video'
|
||||||
|
: 'https://www.youtube.com/embed'
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={`${baseUrl}/${content.id}`}
|
||||||
|
className={
|
||||||
|
'w-full p-4 content-opacity z-10 rounded-md ' +
|
||||||
|
(isTyping ? 'opacity-0' : 'opacity-100')
|
||||||
|
}
|
||||||
|
height={isTyping ? '2rem' : '200px'}
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import { StepBase } from '.'
|
import { StepBase } from '.'
|
||||||
|
|
||||||
export type BubbleStep = TextBubbleStep | ImageBubbleStep
|
export type BubbleStep = TextBubbleStep | ImageBubbleStep | VideoBubbleStep
|
||||||
|
|
||||||
export enum BubbleStepType {
|
export enum BubbleStepType {
|
||||||
TEXT = 'text',
|
TEXT = 'text',
|
||||||
IMAGE = 'image',
|
IMAGE = 'image',
|
||||||
|
VIDEO = 'video',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BubbleStepContent =
|
||||||
|
| TextBubbleContent
|
||||||
|
| ImageBubbleContent
|
||||||
|
| VideoBubbleContent
|
||||||
|
|
||||||
export type TextBubbleStep = StepBase & {
|
export type TextBubbleStep = StepBase & {
|
||||||
type: BubbleStepType.TEXT
|
type: BubbleStepType.TEXT
|
||||||
content: TextBubbleContent
|
content: TextBubbleContent
|
||||||
@@ -17,6 +23,11 @@ export type ImageBubbleStep = StepBase & {
|
|||||||
content?: ImageBubbleContent
|
content?: ImageBubbleContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VideoBubbleStep = StepBase & {
|
||||||
|
type: BubbleStepType.VIDEO
|
||||||
|
content?: VideoBubbleContent
|
||||||
|
}
|
||||||
|
|
||||||
export type TextBubbleContent = {
|
export type TextBubbleContent = {
|
||||||
html: string
|
html: string
|
||||||
richText: unknown[]
|
richText: unknown[]
|
||||||
@@ -26,3 +37,15 @@ export type TextBubbleContent = {
|
|||||||
export type ImageBubbleContent = {
|
export type ImageBubbleContent = {
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum VideoBubbleContentType {
|
||||||
|
URL = 'url',
|
||||||
|
YOUTUBE = 'youtube',
|
||||||
|
VIMEO = 'vimeo',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoBubbleContent = {
|
||||||
|
type?: VideoBubbleContentType
|
||||||
|
url?: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -4972,6 +4972,11 @@ js-cookie@^2.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
js-video-url-parser@^0.5.1:
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-video-url-parser/-/js-video-url-parser-0.5.1.tgz#78fea9bf6944b538276af9658833e48a83054909"
|
||||||
|
integrity sha512-/vwqT67k0AyIGMHAvSOt+n4JfrZWF7cPKgKswDO35yr27GfW4HtjpQVlTx6JLF45QuPm8mkzFHkZgFVnFm4x/w==
|
||||||
|
|
||||||
js-yaml@^3.13.1:
|
js-yaml@^3.13.1:
|
||||||
version "3.14.1"
|
version "3.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||||
|
|||||||
Reference in New Issue
Block a user