♻️(frontend) refactor clipboard logic into dedicated reusable hook

Extract clipboard content logic from UI components into a separate
custom hook to decouple interface elements from clipboard functionality.

Creates a reusable hook that can better adapt to future UX changes
without requiring modifications to individual UI components.
This commit is contained in:
lebaudantoine
2025-08-07 12:15:20 +02:00
committed by aleb_the_flash
parent b6a5b1a805
commit 201069aa4c
16 changed files with 169 additions and 227 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl' import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Bold, Button, Dialog, type DialogProps, P, Text } from '@/primitives' import { Bold, Button, Dialog, type DialogProps, P, Text } from '@/primitives'
@@ -8,6 +8,7 @@ import { css } from '@/styled-system/css'
import { ApiAccessLevel, ApiRoom } from '@/features/rooms/api/ApiRoom' import { ApiAccessLevel, ApiRoom } from '@/features/rooms/api/ApiRoom'
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony' import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
import { formatPinCode } from '@/features/rooms/utils/telephony' import { formatPinCode } from '@/features/rooms/utils/telephony'
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
// fixme - duplication with the InviteDialog // fixme - duplication with the InviteDialog
export const LaterMeetingDialog = ({ export const LaterMeetingDialog = ({
@@ -19,47 +20,18 @@ export const LaterMeetingDialog = ({
const roomUrl = room && getRouteUrl('room', room?.slug) const roomUrl = room && getRouteUrl('room', room?.slug)
const telephony = useTelephony() const telephony = useTelephony()
const [isCopied, setIsCopied] = useState(false)
const [isRoomUrlCopied, setIsRoomUrlCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
useEffect(() => {
if (isRoomUrlCopied) {
const timeout = setTimeout(() => setIsRoomUrlCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isRoomUrlCopied])
const isTelephonyReadyForUse = useMemo(() => { const isTelephonyReadyForUse = useMemo(() => {
return telephony?.enabled && room?.pin_code return telephony?.enabled && room?.pin_code
}, [telephony?.enabled, room?.pin_code]) }, [telephony?.enabled, room?.pin_code])
const clipboardContent = useMemo(() => { const {
if (isTelephonyReadyForUse) { isCopied,
return [ copyRoomToClipboard,
t('clipboard.url', { roomUrl }), isRoomUrlCopied,
t('clipboard.numberAndPin', { copyRoomUrlToClipboard,
phoneNumber: telephony?.internationalPhoneNumber, } = useCopyRoomToClipboard(room || undefined)
pinCode: formatPinCode(room?.pin_code),
}),
].join('\n')
}
return roomUrl
}, [
isTelephonyReadyForUse,
roomUrl,
telephony?.internationalPhoneNumber,
room?.pin_code,
t,
])
return ( return (
<Dialog isOpen={!!room} {...dialogProps} title={t('heading')}> <Dialog isOpen={!!room} {...dialogProps} title={t('heading')}>
@@ -95,10 +67,7 @@ export const LaterMeetingDialog = ({
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'} variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square square
size={'sm'} size={'sm'}
onPress={() => { onPress={copyRoomUrlToClipboard}
navigator.clipboard.writeText(roomUrl)
setIsRoomUrlCopied(true)
}}
aria-label={t('copyUrl')} aria-label={t('copyUrl')}
tooltip={t('copyUrl')} tooltip={t('copyUrl')}
> >
@@ -121,36 +90,31 @@ export const LaterMeetingDialog = ({
{formatPinCode(room?.pin_code)} {formatPinCode(room?.pin_code)}
</Text> </Text>
</div> </div>
{clipboardContent && ( <Button
<Button variant={isCopied ? 'success' : 'tertiaryText'}
variant={isCopied ? 'success' : 'tertiaryText'} size="sm"
size="sm" fullWidth
fullWidth aria-label={t('copy')}
aria-label={t('copy')} style={{
style={{ justifyContent: 'start',
justifyContent: 'start', }}
}} onPress={copyRoomToClipboard}
onPress={() => { data-attr="later-dialog-copy"
navigator.clipboard.writeText(clipboardContent) >
setIsCopied(true) {isCopied ? (
}} <>
data-attr="later-dialog-copy" <RiCheckLine size={18} style={{ marginRight: '8px' }} />
> {t('copied')}
{isCopied ? ( </>
<> ) : (
<RiCheckLine size={18} style={{ marginRight: '8px' }} /> <>
{t('copied')} <RiFileCopyLine
</> style={{ marginRight: '6px', minWidth: '18px' }}
) : ( />
<> {t('copy')}
<RiFileCopyLine </>
style={{ marginRight: '6px', minWidth: '18px' }} )}
/> </Button>
{t('copy')}
</>
)}
</Button>
)}
</div> </div>
) : ( ) : (
<Button <Button
@@ -161,10 +125,7 @@ export const LaterMeetingDialog = ({
style={{ style={{
justifyContent: 'start', justifyContent: 'start',
}} }}
onPress={() => { onPress={copyRoomToClipboard}
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered} onHoverChange={setIsHovered}
data-attr="later-dialog-copy" data-attr="later-dialog-copy"
> >

View File

@@ -10,12 +10,13 @@ import {
RiFileCopyLine, RiFileCopyLine,
RiSpam2Fill, RiSpam2Fill,
} from '@remixicon/react' } from '@remixicon/react'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom' import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony' import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
import { formatPinCode } from '@/features/rooms/utils/telephony' import { formatPinCode } from '@/features/rooms/utils/telephony'
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
// fixme - extract in a proper primitive this dialog without overlay // fixme - extract in a proper primitive this dialog without overlay
const StyledRACDialog = styled(Dialog, { const StyledRACDialog = styled(Dialog, {
@@ -43,22 +44,6 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
const roomData = useRoomData() const roomData = useRoomData()
const roomUrl = getRouteUrl('room', roomData?.slug) const roomUrl = getRouteUrl('room', roomData?.slug)
const [isCopied, setIsCopied] = useState(false)
const [isRoomUrlCopied, setIsRoomUrlCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
useEffect(() => {
if (isRoomUrlCopied) {
const timeout = setTimeout(() => setIsRoomUrlCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isRoomUrlCopied])
const telephony = useTelephony() const telephony = useTelephony()
@@ -66,24 +51,12 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
return telephony?.enabled && roomData?.pin_code return telephony?.enabled && roomData?.pin_code
}, [telephony?.enabled, roomData?.pin_code]) }, [telephony?.enabled, roomData?.pin_code])
const clipboardContent = useMemo(() => { const {
if (isTelephonyReadyForUse) { isCopied,
return [ copyRoomToClipboard,
t('clipboard.url', { roomUrl }), isRoomUrlCopied,
t('clipboard.numberAndPin', { copyRoomUrlToClipboard,
phoneNumber: telephony?.internationalPhoneNumber, } = useCopyRoomToClipboard(roomData)
pinCode: formatPinCode(roomData?.pin_code),
}),
].join('\n')
}
return roomUrl
}, [
isTelephonyReadyForUse,
roomUrl,
telephony?.internationalPhoneNumber,
roomData?.pin_code,
t,
])
return ( return (
<StyledRACDialog {...props}> <StyledRACDialog {...props}>
@@ -138,10 +111,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'} variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
square square
size={'sm'} size={'sm'}
onPress={() => { onPress={copyRoomUrlToClipboard}
navigator.clipboard.writeText(roomUrl)
setIsRoomUrlCopied(true)
}}
aria-label={t('copyUrl')} aria-label={t('copyUrl')}
tooltip={t('copyUrl')} tooltip={t('copyUrl')}
> >
@@ -164,46 +134,38 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
{formatPinCode(roomData?.pin_code)} {formatPinCode(roomData?.pin_code)}
</Text> </Text>
</div> </div>
{clipboardContent && ( <Button
<Button variant={isCopied ? 'success' : 'secondaryText'}
variant={isCopied ? 'success' : 'secondaryText'} size="sm"
size="sm" fullWidth
fullWidth aria-label={t('copy')}
aria-label={t('copy')} style={{
style={{ justifyContent: 'start',
justifyContent: 'start', }}
}} onPress={copyRoomToClipboard}
onPress={() => { data-attr="share-dialog-copy"
navigator.clipboard.writeText(clipboardContent) >
setIsCopied(true) {isCopied ? (
}} <>
data-attr="share-dialog-copy" <RiCheckLine size={18} style={{ marginRight: '8px' }} />
> {t('copied')}
{isCopied ? ( </>
<> ) : (
<RiCheckLine size={18} style={{ marginRight: '8px' }} /> <>
{t('copied')} <RiFileCopyLine
</> style={{ marginRight: '6px', minWidth: '18px' }}
) : ( />
<> {t('copy')}
<RiFileCopyLine </>
style={{ marginRight: '6px', minWidth: '18px' }} )}
/> </Button>
{t('copy')}
</>
)}
</Button>
)}
</div> </div>
) : ( ) : (
<Button <Button
variant={isCopied ? 'success' : 'tertiary'} variant={isCopied ? 'success' : 'tertiary'}
fullWidth fullWidth
aria-label={t('copy')} aria-label={t('copy')}
onPress={() => { onPress={copyRoomToClipboard}
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
data-attr="share-dialog-copy" data-attr="share-dialog-copy"
> >
{isCopied ? ( {isCopied ? (

View File

@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { VStack } from '@/styled-system/jsx' import { VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react' import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
@@ -8,19 +8,11 @@ import { getRouteUrl } from '@/navigation/getRouteUrl'
import { useRoomData } from '../hooks/useRoomData' import { useRoomData } from '../hooks/useRoomData'
import { formatPinCode } from '../../utils/telephony' import { formatPinCode } from '../../utils/telephony'
import { useTelephony } from '../hooks/useTelephony' import { useTelephony } from '../hooks/useTelephony'
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
export const Info = () => { export const Info = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'info' }) const { t } = useTranslation('rooms', { keyPrefix: 'info' })
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const data = useRoomData() const data = useRoomData()
const roomUrl = getRouteUrl('room', data?.slug) const roomUrl = getRouteUrl('room', data?.slug)
@@ -30,24 +22,7 @@ export const Info = () => {
return telephony?.enabled && data?.pin_code return telephony?.enabled && data?.pin_code
}, [telephony?.enabled, data?.pin_code]) }, [telephony?.enabled, data?.pin_code])
const clipboardContent = useMemo(() => { const { isCopied, copyRoomToClipboard } = useCopyRoomToClipboard(data)
if (isTelephonyReadyForUse) {
return [
t('roomInformation.clipboard.url', { roomUrl }),
t('roomInformation.clipboard.numberAndPin', {
phoneNumber: telephony?.internationalPhoneNumber,
pinCode: formatPinCode(data?.pin_code),
}),
].join('\n')
}
return roomUrl
}, [
isTelephonyReadyForUse,
roomUrl,
telephony?.internationalPhoneNumber,
data?.pin_code,
t,
])
return ( return (
<Div <Div
@@ -95,10 +70,7 @@ export const Info = () => {
size="sm" size="sm"
variant={isCopied ? 'success' : 'tertiaryText'} variant={isCopied ? 'success' : 'tertiaryText'}
aria-label={t('roomInformation.button.ariaLabel')} aria-label={t('roomInformation.button.ariaLabel')}
onPress={() => { onPress={copyRoomToClipboard}
navigator.clipboard.writeText(clipboardContent)
setIsCopied(true)
}}
data-attr="copy-info-sidepannel" data-attr="copy-info-sidepannel"
style={{ style={{
marginLeft: '-8px', marginLeft: '-8px',

View File

@@ -0,0 +1,79 @@
import { useTelephony } from './useTelephony'
import { useTranslation } from 'react-i18next'
import { useEffect, useMemo, useState } from 'react'
import { formatPinCode } from '@/features/rooms/utils/telephony'
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { getRouteUrl } from '@/navigation/getRouteUrl'
const COPY_SUCCESS_TIMEOUT = 3000
export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
const telephony = useTelephony()
const { t } = useTranslation('global', { keyPrefix: 'clipboardContent' })
const [isCopied, setIsCopied] = useState(false)
const [isRoomUrlCopied, setIsRoomUrlCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), COPY_SUCCESS_TIMEOUT)
return () => clearTimeout(timeout)
}
}, [isCopied])
useEffect(() => {
if (isRoomUrlCopied) {
const timeout = setTimeout(
() => setIsRoomUrlCopied(false),
COPY_SUCCESS_TIMEOUT
)
return () => clearTimeout(timeout)
}
}, [isRoomUrlCopied])
const roomUrl = useMemo(() => {
return room?.slug ? getRouteUrl('room', room.slug) : ''
}, [room?.slug])
const hasTelephonyInfo = useMemo(() => {
return telephony.enabled && room?.pin_code
}, [telephony.enabled, room?.pin_code])
const content = useMemo(() => {
if (!roomUrl || !room) return ''
if (!hasTelephonyInfo) return roomUrl
return [
t('url', { roomUrl }),
t('numberAndPin', {
phoneNumber: telephony?.internationalPhoneNumber,
pinCode: formatPinCode(room.pin_code),
}),
].join('\n')
}, [roomUrl, hasTelephonyInfo, telephony, room, t])
const copyRoomToClipboard = async () => {
try {
await navigator.clipboard.writeText(content)
setIsCopied(true)
} catch (error) {
console.error(error)
}
}
const copyRoomUrlToClipboard = async () => {
try {
await navigator.clipboard.writeText(roomUrl)
setIsRoomUrlCopied(true)
} catch (error) {
console.error(error)
}
}
return {
isCopied,
copyRoomToClipboard,
isRoomUrlCopied,
copyRoomUrlToClipboard,
}
}

View File

@@ -51,5 +51,9 @@
"ariaLabel": "Hinweis schließen", "ariaLabel": "Hinweis schließen",
"label": "OK" "label": "OK"
} }
},
"clipboardContent": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
} }
} }

View File

@@ -27,10 +27,6 @@
"phone": { "phone": {
"call": "Rufen Sie an:", "call": "Rufen Sie an:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
} }
}, },
"introSlider": { "introSlider": {

View File

@@ -62,10 +62,6 @@
"phone": { "phone": {
"call": "Rufen Sie an:", "call": "Rufen Sie an:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
} }
}, },
"mediaErrorDialog": { "mediaErrorDialog": {
@@ -241,10 +237,6 @@
"phone": { "phone": {
"call": "Rufen Sie an:", "call": "Rufen Sie an:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
} }
} }
}, },

View File

@@ -51,5 +51,9 @@
"ariaLabel": "Close the suggestion", "ariaLabel": "Close the suggestion",
"label": "OK" "label": "OK"
} }
},
"clipboardContent": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
} }
} }

View File

@@ -27,10 +27,6 @@
"phone": { "phone": {
"call": "Call:", "call": "Call:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
} }
}, },
"introSlider": { "introSlider": {

View File

@@ -62,10 +62,6 @@
"phone": { "phone": {
"call": "Call:", "call": "Call:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
} }
}, },
"mediaErrorDialog": { "mediaErrorDialog": {
@@ -241,10 +237,6 @@
"phone": { "phone": {
"call": "Call:", "call": "Call:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
} }
} }
}, },

View File

@@ -51,5 +51,9 @@
"ariaLabel": "Fermer la suggestion", "ariaLabel": "Fermer la suggestion",
"label": "OK" "label": "OK"
} }
},
"clipboardContent": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
} }
} }

View File

@@ -27,10 +27,6 @@
"phone": { "phone": {
"call": "Appelez le :", "call": "Appelez le :",
"pinCode": "Code :" "pinCode": "Code :"
},
"clipboard": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
} }
}, },
"introSlider": { "introSlider": {

View File

@@ -62,10 +62,6 @@
"phone": { "phone": {
"call": "Appelez le :", "call": "Appelez le :",
"pinCode": "Code :" "pinCode": "Code :"
},
"clipboard": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
} }
}, },
"mediaErrorDialog": { "mediaErrorDialog": {
@@ -241,10 +237,6 @@
"phone": { "phone": {
"call": "Appelez le :", "call": "Appelez le :",
"pinCode": "Code :" "pinCode": "Code :"
},
"clipboard": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
} }
} }
}, },

View File

@@ -50,5 +50,9 @@
"ariaLabel": "Sluit de suggestie", "ariaLabel": "Sluit de suggestie",
"label": "OK" "label": "OK"
} }
},
"clipboardContent": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
} }
} }

View File

@@ -27,10 +27,6 @@
"phone": { "phone": {
"call": "Bel:", "call": "Bel:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
} }
}, },
"introSlider": { "introSlider": {

View File

@@ -62,10 +62,6 @@
"phone": { "phone": {
"call": "Bel:", "call": "Bel:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
} }
}, },
"mediaErrorDialog": { "mediaErrorDialog": {
@@ -241,10 +237,6 @@
"phone": { "phone": {
"call": "Bel:", "call": "Bel:",
"pinCode": "Code:" "pinCode": "Code:"
},
"clipboard": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
} }
} }
}, },