feat(engine): ✨ Can edit answers by clicking on it
This commit is contained in:
@ -1,56 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Avatar } from '../avatars/Avatar'
|
||||
import { useFrame } from 'react-frame-component'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import { useHostAvatars } from '../../contexts/HostAvatarsContext'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
|
||||
export const AvatarSideContainer = ({
|
||||
hostAvatarSrc,
|
||||
}: {
|
||||
hostAvatarSrc?: string
|
||||
}) => {
|
||||
const { lastBubblesTopOffset } = useHostAvatars()
|
||||
const { window, document } = useFrame()
|
||||
const [marginBottom, setMarginBottom] = useState(
|
||||
window.innerWidth < 400 ? 38 : 48
|
||||
)
|
||||
type Props = { hostAvatarSrc?: string }
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const isMobile = window.innerWidth < 400
|
||||
setMarginBottom(isMobile ? 38 : 48)
|
||||
})
|
||||
resizeObserver.observe(document.body)
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
export const AvatarSideContainer = forwardRef(
|
||||
({ hostAvatarSrc }: Props, ref: ForwardedRef<unknown>) => {
|
||||
const { document } = useFrame()
|
||||
const [show, setShow] = useState(false)
|
||||
const [avatarTopOffset, setAvatarTopOffset] = useState(0)
|
||||
|
||||
const refreshTopOffset = () => {
|
||||
if (!scrollingSideBlockRef.current || !avatarContainer.current) return
|
||||
const { height } = scrollingSideBlockRef.current.getBoundingClientRect()
|
||||
const { height: avatarHeight } =
|
||||
avatarContainer.current.getBoundingClientRect()
|
||||
setAvatarTopOffset(height - avatarHeight)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
const scrollingSideBlockRef = useRef<HTMLDivElement>(null)
|
||||
const avatarContainer = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(ref, () => ({
|
||||
refreshTopOffset,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="flex w-6 xs:w-10 mr-2 flex-shrink-0 items-center">
|
||||
<TransitionGroup>
|
||||
{lastBubblesTopOffset
|
||||
.filter((n) => n !== -1)
|
||||
.map((topOffset, idx) => (
|
||||
<CSSTransition
|
||||
key={idx}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<div
|
||||
className="absolute w-6 h-6 xs:w-10 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
|
||||
style={{
|
||||
top: `calc(${topOffset}px - ${marginBottom}px)`,
|
||||
transition: 'top 350ms ease-out',
|
||||
}}
|
||||
>
|
||||
<Avatar avatarSrc={hostAvatarSrc} />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
setShow(true)
|
||||
const resizeObserver = new ResizeObserver(refreshTopOffset)
|
||||
resizeObserver.observe(document.body)
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-6 xs:w-10 mr-2 mb-2 flex-shrink-0 items-center relative "
|
||||
ref={scrollingSideBlockRef}
|
||||
>
|
||||
<CSSTransition
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
in={show}
|
||||
unmountOnExit
|
||||
>
|
||||
<div
|
||||
className="absolute w-6 xs:w-10 h-6 xs:h-10 mb-4 xs:mb-2 flex items-center top-0"
|
||||
ref={avatarContainer}
|
||||
style={{
|
||||
top: `${avatarTopOffset}px`,
|
||||
transition: 'top 350ms ease-out, opacity 500ms',
|
||||
}}
|
||||
>
|
||||
<Avatar avatarSrc={hostAvatarSrc} />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import {
|
||||
isBubbleStep,
|
||||
@ -16,7 +14,9 @@ import { executeIntegration } from 'services/integration'
|
||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||
import { parseVariables } from 'index'
|
||||
import { useAnswers } from 'contexts/AnswersContext'
|
||||
import { Step } from 'models'
|
||||
import { BubbleStep, InputStep, Step } from 'models'
|
||||
import { HostBubble } from './ChatStep/bubbles/HostBubble'
|
||||
import { InputChatStep } from './ChatStep/InputChatStep'
|
||||
|
||||
type ChatBlockProps = {
|
||||
steps: Step[]
|
||||
@ -41,6 +41,13 @@ export const ChatBlock = ({
|
||||
} = useTypebot()
|
||||
const { resultValues } = useAnswers()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||
const bubbleSteps = displayedSteps.filter((step) =>
|
||||
isBubbleStep(step)
|
||||
) as BubbleStep[]
|
||||
const inputSteps = displayedSteps.filter((step) =>
|
||||
isInputStep(step)
|
||||
) as InputStep[]
|
||||
const avatarSideContainerRef = useRef<any>()
|
||||
|
||||
useEffect(() => {
|
||||
const nextStep = steps[startStepIndex]
|
||||
@ -49,6 +56,7 @@ export const ChatBlock = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
avatarSideContainerRef.current?.refreshTopOffset()
|
||||
onScroll()
|
||||
onNewStepDisplayed()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -118,33 +126,49 @@ export const ChatBlock = ({
|
||||
}
|
||||
|
||||
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
||||
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<HostAvatarsContext>
|
||||
{(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
||||
<AvatarSideContainer
|
||||
hostAvatarSrc={
|
||||
avatarSrc && parseVariables(typebot.variables)(avatarSrc)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col w-full min-w-0">
|
||||
<div className="flex flex-col w-full min-w-0">
|
||||
<div className="flex">
|
||||
{(typebot.theme.chat.hostAvatar?.isEnabled ?? true) && (
|
||||
<AvatarSideContainer
|
||||
ref={avatarSideContainerRef}
|
||||
hostAvatarSrc={
|
||||
avatarSrc && parseVariables(typebot.variables)(avatarSrc)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TransitionGroup>
|
||||
{displayedSteps
|
||||
.filter((step) => isInputStep(step) || isBubbleStep(step))
|
||||
.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<ChatStep step={step} onTransitionEnd={displayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
{bubbleSteps.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<HostBubble step={step} onTransitionEnd={displayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</HostAvatarsContext>
|
||||
<TransitionGroup>
|
||||
{inputSteps.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<InputChatStep
|
||||
step={step}
|
||||
onTransitionEnd={displayNextStep}
|
||||
hasAvatar={typebot.theme.chat.hostAvatar?.isEnabled ?? true}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,31 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
||||
import { InputStep, InputStepType, Step } from 'models'
|
||||
import { InputStep, InputStepType } from 'models'
|
||||
import { GuestBubble } from './bubbles/GuestBubble'
|
||||
import { TextForm } from './inputs/TextForm'
|
||||
import { byId, isBubbleStep, isInputStep } from 'utils'
|
||||
import { byId } from 'utils'
|
||||
import { DateForm } from './inputs/DateForm'
|
||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||
import { HostBubble } from './bubbles/HostBubble'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { parseVariables } from 'index'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
|
||||
export const ChatStep = ({
|
||||
export const InputChatStep = ({
|
||||
step,
|
||||
hasAvatar,
|
||||
onTransitionEnd,
|
||||
}: {
|
||||
step: Step
|
||||
step: InputStep
|
||||
hasAvatar: boolean
|
||||
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
||||
}) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { addAnswer } = useAnswers()
|
||||
const [answer, setAnswer] = useState<string>()
|
||||
const [isEditting, setIsEditting] = useState(false)
|
||||
|
||||
const handleInputSubmit = (
|
||||
content: string,
|
||||
isRetry: boolean,
|
||||
variableId?: string
|
||||
) => {
|
||||
const { variableId } = step.options
|
||||
const defaultValue =
|
||||
variableId && typebot.variables.find(byId(variableId))?.value
|
||||
|
||||
const handleSubmit = (content: string) => {
|
||||
setAnswer(content)
|
||||
const isRetry = !isInputValid(content, step.type)
|
||||
if (!isRetry)
|
||||
addAnswer({
|
||||
stepId: step.id,
|
||||
@ -33,38 +38,13 @@ export const ChatStep = ({
|
||||
content,
|
||||
variableId: variableId ?? null,
|
||||
})
|
||||
onTransitionEnd(content, isRetry)
|
||||
if (!isEditting) onTransitionEnd(content, isRetry)
|
||||
setIsEditting(false)
|
||||
}
|
||||
|
||||
if (isBubbleStep(step))
|
||||
return <HostBubble step={step} onTransitionEnd={onTransitionEnd} />
|
||||
if (isInputStep(step))
|
||||
return <InputChatStep step={step} onSubmit={handleInputSubmit} />
|
||||
return <span>No step</span>
|
||||
}
|
||||
|
||||
const InputChatStep = ({
|
||||
step,
|
||||
onSubmit,
|
||||
}: {
|
||||
step: InputStep
|
||||
onSubmit: (value: string, isRetry: boolean, variableId?: string) => void
|
||||
}) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { addNewAvatarOffset } = useHostAvatars()
|
||||
const [answer, setAnswer] = useState<string>()
|
||||
const { variableId } = step.options
|
||||
const defaultValue =
|
||||
variableId && typebot.variables.find(byId(variableId))?.value
|
||||
|
||||
useEffect(() => {
|
||||
addNewAvatarOffset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
setAnswer(value)
|
||||
onSubmit(value, !isInputValid(value, step.type), step.options.variableId)
|
||||
const handleGuestBubbleClick = () => {
|
||||
setAnswer(undefined)
|
||||
setIsEditting(true)
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
@ -74,9 +54,30 @@ const InputChatStep = ({
|
||||
message={answer}
|
||||
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||
avatarSrc={avatarUrl && parseVariables(typebot.variables)(avatarUrl)}
|
||||
onClick={handleGuestBubbleClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{hasAvatar && (
|
||||
<div className="flex w-6 xs:w-10 h-6 xs:h-10 mr-2 mb-2 mt-1 flex-shrink-0 items-center" />
|
||||
)}
|
||||
<Input step={step} onSubmit={handleSubmit} defaultValue={defaultValue} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
step,
|
||||
onSubmit,
|
||||
defaultValue,
|
||||
}: {
|
||||
step: InputStep
|
||||
onSubmit: (value: string) => void
|
||||
defaultValue?: string
|
||||
}) => {
|
||||
switch (step.type) {
|
||||
case InputStepType.TEXT:
|
||||
case InputStepType.NUMBER:
|
||||
@ -84,15 +85,11 @@ const InputChatStep = ({
|
||||
case InputStepType.URL:
|
||||
case InputStepType.PHONE:
|
||||
return (
|
||||
<TextForm
|
||||
step={step}
|
||||
onSubmit={handleSubmit}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
<TextForm step={step} onSubmit={onSubmit} defaultValue={defaultValue} />
|
||||
)
|
||||
case InputStepType.DATE:
|
||||
return <DateForm options={step.options} onSubmit={handleSubmit} />
|
||||
return <DateForm options={step.options} onSubmit={onSubmit} />
|
||||
case InputStepType.CHOICE:
|
||||
return <ChoiceForm step={step} onSubmit={handleSubmit} />
|
||||
return <ChoiceForm step={step} onSubmit={onSubmit} />
|
||||
}
|
||||
}
|
@ -6,18 +6,20 @@ interface Props {
|
||||
message: string
|
||||
showAvatar: boolean
|
||||
avatarSrc?: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const GuestBubble = ({
|
||||
message,
|
||||
showAvatar,
|
||||
avatarSrc,
|
||||
onClick,
|
||||
}: Props): JSX.Element => {
|
||||
return (
|
||||
<CSSTransition classNames="bubble" timeout={1000}>
|
||||
<div className="flex justify-end mb-2 items-end">
|
||||
<div className="flex justify-end mb-2 items-end" onClick={onClick}>
|
||||
<span
|
||||
className="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
||||
className="px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble cursor-pointer hover:brightness-90 active:brightness-75"
|
||||
data-testid="guest-bubble"
|
||||
>
|
||||
{message}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ImageBubbleStep } from 'models'
|
||||
import { TypingContent } from './TypingContent'
|
||||
@ -16,7 +15,6 @@ export const mediaLoadingFallbackTimeout = 5000
|
||||
|
||||
export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { updateLastAvatarOffset } = useHostAvatars()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const image = useRef<HTMLImageElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
@ -27,7 +25,6 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
sendAvatarOffset()
|
||||
showContentAfterMediaLoad()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
@ -48,19 +45,10 @@ export const ImageBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const onTypingEnd = () => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
sendAvatarOffset()
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}
|
||||
|
||||
const sendAvatarOffset = () => {
|
||||
if (!messageContainer.current) return
|
||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
||||
updateLastAvatarOffset(
|
||||
messageContainer.current.offsetTop + containerDimensions.height
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" ref={messageContainer}>
|
||||
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { BubbleStepType, TextBubbleStep } from 'models'
|
||||
import { computeTypingTimeout } from 'services/chat'
|
||||
@ -21,7 +20,6 @@ const defaultTypingEmulation = {
|
||||
|
||||
export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { updateLastAvatarOffset } = useHostAvatars()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
@ -32,7 +30,6 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
sendAvatarOffset()
|
||||
const typingTimeout = computeTypingTimeout(
|
||||
step.content.plainText,
|
||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||
@ -46,19 +43,10 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const onTypingEnd = () => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
sendAvatarOffset()
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}
|
||||
|
||||
const sendAvatarOffset = () => {
|
||||
if (!messageContainer.current) return
|
||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
||||
updateLastAvatarOffset(
|
||||
messageContainer.current.offsetTop + containerDimensions.height
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" ref={messageContainer}>
|
||||
<div className="flex mb-2 w-full items-center">
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
Variable,
|
||||
@ -21,12 +20,10 @@ 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(() => {
|
||||
sendAvatarOffset()
|
||||
showContentAfterMediaLoad()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
@ -41,19 +38,10 @@ export const VideoBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const onTypingEnd = () => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
sendAvatarOffset()
|
||||
onTransitionEnd()
|
||||
}, showAnimationDuration)
|
||||
}
|
||||
|
||||
const sendAvatarOffset = () => {
|
||||
if (!messageContainer.current) return
|
||||
const containerDimensions = messageContainer.current.getBoundingClientRect()
|
||||
updateLastAvatarOffset(
|
||||
messageContainer.current.offsetTop + containerDimensions.height
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" ref={messageContainer}>
|
||||
<div className="flex mb-2 w-full lg:w-11/12 items-center">
|
||||
|
@ -1 +1 @@
|
||||
export { ChatStep } from './ChatStep'
|
||||
export { InputChatStep } from './InputChatStep'
|
||||
|
@ -40,7 +40,7 @@ export const TextForm = ({ step, onSubmit, defaultValue }: TextFormProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full lg:w-4/6">
|
||||
<div className="flex flex-col w-full lg:w-4/6 mb-2">
|
||||
<div className="flex items-center">
|
||||
<form
|
||||
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
||||
|
@ -1,39 +0,0 @@
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
|
||||
// This context just keeps track of the top offset of host avatar
|
||||
const hostAvatarsContext = createContext<{
|
||||
lastBubblesTopOffset: number[]
|
||||
addNewAvatarOffset: () => void
|
||||
updateLastAvatarOffset: (newOffset: number) => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const HostAvatarsContext = ({ children }: { children: ReactNode }) => {
|
||||
const [lastBubblesTopOffset, setLastBubblesTopOffset] = useState<number[]>([
|
||||
-1,
|
||||
])
|
||||
|
||||
const updateLastAvatarOffset = (newOffset: number) => {
|
||||
const offsets = [...lastBubblesTopOffset]
|
||||
offsets[offsets.length - 1] = newOffset
|
||||
setLastBubblesTopOffset(offsets)
|
||||
}
|
||||
|
||||
const addNewAvatarOffset = () =>
|
||||
setLastBubblesTopOffset([...lastBubblesTopOffset, -1])
|
||||
|
||||
return (
|
||||
<hostAvatarsContext.Provider
|
||||
value={{
|
||||
lastBubblesTopOffset,
|
||||
updateLastAvatarOffset,
|
||||
addNewAvatarOffset,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</hostAvatarsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHostAvatars = () => useContext(hostAvatarsContext)
|
Reference in New Issue
Block a user