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

View File

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

View File

@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
@@ -8,19 +8,11 @@ import { getRouteUrl } from '@/navigation/getRouteUrl'
import { useRoomData } from '../hooks/useRoomData'
import { formatPinCode } from '../../utils/telephony'
import { useTelephony } from '../hooks/useTelephony'
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
export const 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 roomUrl = getRouteUrl('room', data?.slug)
@@ -30,24 +22,7 @@ export const Info = () => {
return telephony?.enabled && data?.pin_code
}, [telephony?.enabled, data?.pin_code])
const clipboardContent = useMemo(() => {
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,
])
const { isCopied, copyRoomToClipboard } = useCopyRoomToClipboard(data)
return (
<Div
@@ -95,10 +70,7 @@ export const Info = () => {
size="sm"
variant={isCopied ? 'success' : 'tertiaryText'}
aria-label={t('roomInformation.button.ariaLabel')}
onPress={() => {
navigator.clipboard.writeText(clipboardContent)
setIsCopied(true)
}}
onPress={copyRoomToClipboard}
data-attr="copy-info-sidepannel"
style={{
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",
"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": {
"call": "Rufen Sie an:",
"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": {

View File

@@ -62,10 +62,6 @@
"phone": {
"call": "Rufen Sie an:",
"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": {
@@ -241,10 +237,6 @@
"phone": {
"call": "Rufen Sie an:",
"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",
"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": {
"call": "Call:",
"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": {

View File

@@ -62,10 +62,6 @@
"phone": {
"call": "Call:",
"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": {
@@ -241,10 +237,6 @@
"phone": {
"call": "Call:",
"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",
"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": {
"call": "Appelez le :",
"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": {

View File

@@ -62,10 +62,6 @@
"phone": {
"call": "Appelez le :",
"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": {
@@ -241,10 +237,6 @@
"phone": {
"call": "Appelez le :",
"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",
"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": {
"call": "Bel:",
"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": {

View File

@@ -62,10 +62,6 @@
"phone": {
"call": "Bel:",
"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": {
@@ -241,10 +237,6 @@
"phone": {
"call": "Bel:",
"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}}"
}
}
},