Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine ddab4bf366 🚧(backend) document a resource server integration
/!\ Not for merging

Meet is calling Regie during user authentication, passing the access token
from AgentConnect to the resource server.

/!\ The access token is valid for 60s. After this time, you won't be
able to inspect it, neither get data from the resource server.

Why do you need to update scopes?

When introspecting an access token with AgentConnect, it verifies that the
relevant scopes were requested during authentication. Users need the
"groups" scope to access the Regie resource server.

Note: AgentConnect controls which FS has access to which scopes. A FS cannot
request the "groups" scope without approval from AC. Contact Benoit to add the
"groups" scope to your client.

In this example, I query a random endpoint (/users/me), but the entire Regie
API is accessible using the AC access token. This is temporary. Once we have
built something useful with a few FS, I'll restrict the resource server to
fewer endpoints, focusing on those related to groups.
2024-08-30 12:40:12 +02:00
63 changed files with 332 additions and 1617 deletions
@@ -65,6 +65,19 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
- Exception: Raised when user creation is not allowed and no existing user is found.
"""
try:
response = requests.get(
"https://desk-staging.beta.numerique.gouv.fr/api/v1.0/users/me",
headers={"Authorization": f"Bearer {access_token}"},
)
response.raise_for_status()
api_data = response.json()
print(api_data) # Process the Regie data as needed
except requests.exceptions.RequestException as e:
print(f"API request failed: {e}")
user_info = self.get_userinfo(access_token, id_token, payload)
sub = user_info.get("sub")
+1 -7
View File
@@ -4,7 +4,6 @@ Utils functions used in the core app
# ruff: noqa:S311
import hashlib
import json
import random
from typing import Optional
@@ -25,11 +24,7 @@ def generate_color(identity: str) -> str:
range and ensure predictability.
"""
# ruff: noqa:S324
identity_hash = hashlib.sha1(identity.encode("utf-8"))
# Keep only hash's last 16 bits, collisions are not a concern
seed = int(identity_hash.hexdigest(), 16) & 0xFFFF
random.seed(seed)
random.seed(hash(identity))
hue = random.randint(0, 360)
saturation = random.randint(50, 75)
lightness = random.randint(25, 60)
@@ -53,7 +48,6 @@ def generate_token(room: str, user, username: Optional[str] = None) -> str:
room=room,
room_join=True,
room_admin=True,
room_record=True,
can_update_own_metadata=True,
can_publish_sources=[
"camera",
+5 -5
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.5"
version = "0.1.4"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.10",
"boto3==1.35.5",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
@@ -71,17 +71,17 @@ dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2024.7.1",
"ipdb==0.13.13",
"ipython==8.27.0",
"ipython==8.26.0",
"pyfakefs==5.6.0",
"pylint-django==2.5.5",
"pylint==3.2.7",
"pylint==3.2.6",
"pytest-cov==5.0.0",
"pytest-django==4.8.0",
"pytest==8.3.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.3",
"ruff==0.6.3",
"ruff==0.6.2",
"types-requests==2.32.0.20240712",
]
+44 -106
View File
@@ -1,17 +1,16 @@
{
"name": "meet",
"version": "0.1.5",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.5",
"version": "0.1.4",
"dependencies": {
"@livekit/components-react": "2.3.3",
"@livekit/components-styles": "1.0.12",
"@pandacss/preset-panda": "0.41.0",
"@react-aria/toast": "3.0.0-beta.15",
"@remixicon/react": "4.2.0",
"@tanstack/react-query": "5.49.2",
"hoofd": "1.7.1",
@@ -24,7 +23,6 @@
"react-aria-components": "1.2.1",
"react-dom": "18.2.0",
"react-i18next": "14.1.3",
"use-sound": "^4.0.3",
"valtio": "1.13.2",
"wouter": "3.3.0"
},
@@ -1109,9 +1107,9 @@
"dev": true
},
"node_modules/@internationalized/date": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz",
"integrity": "sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==",
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz",
"integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -1973,35 +1971,35 @@
}
},
"node_modules/@react-aria/i18n": {
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.2.tgz",
"integrity": "sha512-PvEyC6JWylTpe8dQEWqQwV6GiA+pbTxHQd//BxtMSapRW3JT9obObAnb/nFhj3HthkUvqHyj0oO1bfeN+mtD8A==",
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.11.1.tgz",
"integrity": "sha512-vuiBHw1kZruNMYeKkTGGnmPyMnM5T+gT8bz97H1FqIq1hQ6OPzmtBZ6W6l6OIMjeHI5oJo4utTwfZl495GALFQ==",
"dependencies": {
"@internationalized/date": "^3.5.5",
"@internationalized/date": "^3.5.4",
"@internationalized/message": "^3.1.4",
"@internationalized/number": "^3.5.3",
"@internationalized/string": "^3.2.3",
"@react-aria/ssr": "^3.9.5",
"@react-aria/utils": "^3.25.2",
"@react-types/shared": "^3.24.1",
"@react-aria/ssr": "^3.9.4",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.22.2",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.2.tgz",
"integrity": "sha512-xE/77fRVSlqHp2sfkrMeNLrqf2amF/RyuAS6T5oDJemRSgYM3UoxTbWjucPhfnoW7r32pFPHHgz4lbdX8xqD/g==",
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz",
"integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==",
"dependencies": {
"@react-aria/ssr": "^3.9.5",
"@react-aria/utils": "^3.25.2",
"@react-types/shared": "^3.24.1",
"@react-aria/ssr": "^3.9.4",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/label": {
@@ -2017,20 +2015,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/landmark": {
"version": "3.0.0-beta.15",
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.0-beta.15.tgz",
"integrity": "sha512-EEABy0IFzqoS7r11HoD2YwiGR5LFw4kWDFTFUFwJkRP5tHEzsrEgkKPSPJXScQr5m7tenV6j0/Zzl1+w1ki3lA==",
"dependencies": {
"@react-aria/utils": "^3.25.2",
"@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-aria/link": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.7.1.tgz",
@@ -2304,9 +2288,9 @@
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.5",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz",
"integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==",
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz",
"integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
@@ -2314,7 +2298,7 @@
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/switch": {
@@ -2417,24 +2401,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/toast": {
"version": "3.0.0-beta.15",
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.0-beta.15.tgz",
"integrity": "sha512-bg6ZXq4B5JYVt3GXVlf075tQOakcfumbDLnUMaijez4BhacuxF01+IvBPzAVEsARVNXfELoXa7Frb2K54Wgvtw==",
"dependencies": {
"@react-aria/i18n": "^3.12.2",
"@react-aria/interactions": "^3.22.2",
"@react-aria/landmark": "3.0.0-beta.15",
"@react-aria/utils": "^3.25.2",
"@react-stately/toast": "3.0.0-beta.5",
"@react-types/button": "^3.9.6",
"@react-types/shared": "^3.24.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-aria/toggle": {
"version": "3.10.4",
"resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.10.4.tgz",
@@ -2503,18 +2469,18 @@
}
},
"node_modules/@react-aria/utils": {
"version": "3.25.2",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.2.tgz",
"integrity": "sha512-GdIvG8GBJJZygB4L2QJP1Gabyn2mjFsha73I2wSe+o4DYeGWoJiMZRM06PyTIxLH4S7Sn7eVDtsSBfkc2VY/NA==",
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz",
"integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==",
"dependencies": {
"@react-aria/ssr": "^3.9.5",
"@react-stately/utils": "^3.10.3",
"@react-types/shared": "^3.24.1",
"@react-aria/ssr": "^3.9.4",
"@react-stately/utils": "^3.10.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/visually-hidden": {
@@ -2860,18 +2826,6 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-stately/toast": {
"version": "3.0.0-beta.5",
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.0.0-beta.5.tgz",
"integrity": "sha512-MEdQwcKsexlcJ4YQZ9cN5QSIqTlGGdgC5auzSKXXoq15DHuo4mtHfLzXPgcMDYOhOdmyphMto8Vt+21UL2FOrw==",
"dependencies": {
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-stately/toggle": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.7.4.tgz",
@@ -2914,14 +2868,14 @@
}
},
"node_modules/@react-stately/utils": {
"version": "3.10.3",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.3.tgz",
"integrity": "sha512-moClv7MlVSHpbYtQIkm0Cx+on8Pgt1XqtPx6fy9rQFb2DNc9u1G3AUVnqA17buOkH1vLxAtX4MedlxMWyRCYYA==",
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz",
"integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-stately/virtualizer": {
@@ -2950,14 +2904,14 @@
}
},
"node_modules/@react-types/button": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.6.tgz",
"integrity": "sha512-8lA+D5JLbNyQikf8M/cPP2cji91aVTcqjrGpDqI7sQnaLFikM8eFR6l1ZWGtZS5MCcbfooko77ha35SYplSQvw==",
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.9.4.tgz",
"integrity": "sha512-raeQBJUxBp0axNF74TXB8/H50GY8Q3eV6cEKMbZFP1+Dzr09Ngv0tJBeW0ewAxAguNH5DRoMUAUGIXtSXskVdA==",
"dependencies": {
"@react-types/shared": "^3.24.1"
"@react-types/shared": "^3.23.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-types/calendar": {
@@ -3167,11 +3121,11 @@
}
},
"node_modules/@react-types/shared": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.24.1.tgz",
"integrity": "sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==",
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz",
"integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-types/slider": {
@@ -6696,11 +6650,6 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"dev": true
},
"node_modules/howler": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
"integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -10168,17 +10117,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sound": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/use-sound/-/use-sound-4.0.3.tgz",
"integrity": "sha512-L205pEUFIrLsGYsCUKHQVCt0ajs//YQOFbEQeNwaWaqQj3y3st4SuR+rvpMHLmv8hgTcfUFlvMQawZNI3OE18w==",
"dependencies": {
"howler": "^2.1.3"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
+1 -3
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.5",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -16,7 +16,6 @@
"@livekit/components-react": "2.3.3",
"@livekit/components-styles": "1.0.12",
"@pandacss/preset-panda": "0.41.0",
"@react-aria/toast": "3.0.0-beta.15",
"@remixicon/react": "4.2.0",
"@tanstack/react-query": "5.49.2",
"hoofd": "1.7.1",
@@ -29,7 +28,6 @@
"react-aria-components": "1.2.1",
"react-dom": "18.2.0",
"react-i18next": "14.1.3",
"use-sound": "4.0.3",
"valtio": "1.13.2",
"wouter": "3.3.0"
},
-11
View File
@@ -75,17 +75,6 @@ const config: Config = {
'50%': { height: '4px' },
'100%': { height: '8px' },
},
wave_hand: {
'0%': { transform: 'rotate(0deg)' },
'20%': { transform: 'rotate(-20deg)' },
'80%': { transform: 'rotate(20deg)' },
'100%': { transform: 'rotate(0)' },
},
pulse_mic: {
'0%': { color: 'primary', opacity: '1' },
'50%': { color: 'primary', opacity: '0.8' },
'100%': { color: 'primary', opacity: '1' },
},
},
tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff.
Binary file not shown.
@@ -27,17 +27,12 @@ export const SoundTester = () => {
return (
<>
<Button
invisible
onPress={() => {
audioRef?.current?.play()
setIsPlaying(true)
}}
size="sm"
isDisabled={isPlaying}
fullWidth
style={{
color: isPlaying ? 'var(--colors-primary)' : undefined,
}}
>
{isPlaying ? t('audio.speakers.ongoingTest') : t('audio.speakers.test')}
</Button>
@@ -1,9 +1,8 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Button, Dialog, type DialogProps, P, Text } from '@/primitives'
import { Div, Button, Dialog, Input, type DialogProps, P } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
// fixme - duplication with the InviteDialog
export const LaterMeetingDialog = ({
@@ -12,17 +11,19 @@ export const LaterMeetingDialog = ({
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('home')
const roomUrl = getRouteUrl('room', roomId)
const [isCopied, setIsCopied] = useState(false)
const copyLabel = t('laterMeetingDialog.copy')
const copiedLabel = t('laterMeetingDialog.copied')
const [copyLinkLabel, setCopyLinkLabel] = useState(copyLabel)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
if (copyLinkLabel == copiedLabel) {
const timeout = setTimeout(() => {
setCopyLinkLabel(copyLabel)
}, 5000)
return () => {
clearTimeout(timeout)
}
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
}, [copyLinkLabel, copyLabel, copiedLabel])
return (
<Dialog
@@ -31,62 +32,31 @@ export const LaterMeetingDialog = ({
title={t('laterMeetingDialog.heading')}
>
<P>{t('laterMeetingDialog.description')}</P>
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('laterMeetingDialog.copy')}
style={{
justifyContent: 'start',
}}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered}
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('laterMeetingDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('laterMeetingDialog.copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
<HStack>
<div
style={{
backgroundColor: '#d9e5ff',
borderRadius: '50%',
padding: '4px',
marginTop: '1rem',
}}
>
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
</div>
<Text variant="sm" style={{ marginTop: '1rem' }}>
{t('laterMeetingDialog.permissions')}
</Text>
<HStack alignItems="stretch" gap="gutter">
<Div flex="1">
<Input
type="text"
aria-label={t('laterMeetingDialog.inputLabel')}
value={roomUrl}
readOnly
onClick={(e) => {
e.currentTarget.select()
}}
/>
</Div>
<Div minWidth="8rem">
<Button
variant="primary"
size="sm"
fullWidth
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setCopyLinkLabel(copiedLabel)
}}
>
{copyLinkLabel}
</Button>
</Div>
</HStack>
</Dialog>
)
@@ -27,6 +27,8 @@ export const Home = () => {
const { mutateAsync: createRoom } = useCreateRoom()
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
console.log(laterRoomId)
return (
<UserAware>
<Screen>
@@ -1,101 +0,0 @@
import { useEffect } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
import { isMobileBrowser } from '@livekit/components-core'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
useEffect(() => {
const showJoinNotification = (participant: Participant) => {
if (isMobileBrowser()) {
return
}
triggerNotificationSound(NotificationType.Joined)
toastQueue.add(
{
participant,
type: NotificationType.Joined,
},
{
timeout: 5000,
}
)
}
room.on(RoomEvent.ParticipantConnected, showJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const removeJoinNotification = (participant: Participant) => {
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Joined
)
if (existingToast) {
toastQueue.close(existingToast.key)
}
}
room.on(RoomEvent.ParticipantDisconnected, removeJoinNotification)
return () => {
room.off(RoomEvent.ParticipantConnected, removeJoinNotification)
}
}, [room])
// fixme - close all related toasters when hands are lowered remotely
useEffect(() => {
const decoder = new TextDecoder()
const handleNotificationReceived = (
payload: Uint8Array,
participant?: RemoteParticipant
) => {
if (!participant) {
return
}
if (isMobileBrowser()) {
return
}
const notification = decoder.decode(payload)
const existingToast = toastQueue.visibleToasts.find(
(toast) =>
toast.content.participant === participant &&
toast.content.type === NotificationType.Raised
)
if (existingToast && notification === NotificationType.Lowered) {
toastQueue.close(existingToast.key)
return
}
if (!existingToast && notification === NotificationType.Raised) {
triggerNotificationSound(NotificationType.Raised)
toastQueue.add(
{
participant,
type: NotificationType.Raised,
},
{ timeout: 5000 }
)
}
}
room.on(RoomEvent.DataReceived, handleNotificationReceived)
return () => {
room.off(RoomEvent.DataReceived, handleNotificationReceived)
}
}, [room, triggerNotificationSound])
return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
<ToastProvider />
</Div>
)
}
@@ -1,6 +0,0 @@
export enum NotificationType {
Joined = 'joined',
Default = 'default',
Raised = 'raised',
Lowered = 'lowered',
}
@@ -1,58 +0,0 @@
import { useToast } from '@react-aria/toast'
import { Button } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { ToastState } from '@react-stately/toast'
import { styled } from '@/styled-system/jsx'
import { useRef } from 'react'
import { ToastData } from './ToastProvider'
import type { QueuedToast } from '@react-stately/toast'
export const StyledToastContainer = styled('div', {
base: {
margin: 0.5,
boxShadow:
'rgba(0, 0, 0, 0.5) 0px 4px 8px 0px, rgba(0, 0, 0, 0.3) 0px 6px 20px 4px',
backgroundColor: '#494c4f',
color: 'white',
borderRadius: '8px',
'&[data-entering]': { animation: 'fade 200ms' },
'&[data-exiting]': { animation: 'fade 150ms reverse ease-in' },
width: 'fit-content',
marginLeft: 'auto',
},
})
const StyledToast = styled('div', {
base: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '1rem',
padding: '10px',
},
})
export interface ToastProps {
key: string
toast: QueuedToast<ToastData>
state: ToastState<ToastData>
}
export function Toast({ state, ...props }: ToastProps) {
const ref = useRef(null)
const { toastProps, contentProps, closeButtonProps } = useToast(
props,
state,
ref
)
return (
<StyledToastContainer {...toastProps} ref={ref}>
<StyledToast>
<div {...contentProps}>{props.toast.content?.message} machine a</div>
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine color="white" />
</Button>
</StyledToast>
</StyledToastContainer>
)
}
@@ -1,73 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { Button as RACButton } from 'react-aria-components'
import { Track } from 'livekit-client'
import Source = Track.Source
import { useMaybeLayoutContext } from '@livekit/components-react'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack, styled } from '@/styled-system/jsx'
import { Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
const ClickableToast = styled(RACButton, {
base: {
cursor: 'pointer',
display: 'flex',
borderRadius: 'inherit',
},
})
export function ToastJoined({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const layoutContext = useMaybeLayoutContext()
const participant = props.toast.content.participant
const trackReference = {
participant,
publication: participant.getTrackPublication(Source.Camera),
source: Source.Camera,
}
const pinParticipant = () => {
layoutContext?.pin.dispatch?.({
msg: 'set_pin',
trackReference,
})
}
return (
<StyledToastContainer {...toastProps} ref={ref}>
<ClickableToast
ref={ref}
onPress={(e) => {
pinParticipant()
closeButtonProps.onPress?.(e)
}}
>
<HStack justify="center" alignItems="center" {...contentProps}>
<Div display="flex" overflow="hidden" width="128" height="72">
<ParticipantTile
trackRef={trackReference}
disableSpeakingIndicator={true}
disableMetadata={true}
style={{
borderRadius: '7px 0 0 7px',
width: '100%',
}}
/>
</Div>
<Div padding={20} {...titleProps}>
{t('joined.description', {
name: participant.name || t('defaultName'),
})}
</Div>
</HStack>
</ClickableToast>
</StyledToastContainer>
)
}
@@ -1,28 +0,0 @@
/* eslint-disable react-refresh/only-export-components */
import { ToastQueue, useToastQueue } from '@react-stately/toast'
import { ToastRegion } from './ToastRegion'
import { Participant } from 'livekit-client'
import { NotificationType } from '../NotificationType'
export interface ToastData {
participant: Participant
type: NotificationType
message?: string
}
// Using a global queue for toasts allows for centralized management and queuing of notifications
// from anywhere in the app, providing greater flexibility in complex scenarios.
export const toastQueue = new ToastQueue<ToastData>({
maxVisibleToasts: 5,
})
export const ToastProvider = ({ ...props }) => {
const state = useToastQueue<ToastData>(toastQueue)
return (
<>
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
)
}
@@ -1,66 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { Button, Div } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiHand } from '@remixicon/react'
import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction'
export function ToastRaised({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
)
const participant = props.toast.content.participant
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
<RiHand
color="white"
style={{
marginRight: '1rem',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
<Div {...titleProps} marginRight={0.5}>
{t('raised.description', {
name: participant.name || t('defaultName'),
})}
</Div>
{!isParticipantsOpen && (
<Button
size="sm"
variant="text"
style={{
color: '#60a5fa',
}}
onPress={(e) => {
toggleParticipants()
closeButtonProps.onPress?.(e)
}}
>
{t('raised.cta')}
</Button>
)}
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine size={18} color="white" />
</Button>
</HStack>
</StyledToastContainer>
)
}
@@ -1,30 +0,0 @@
import { AriaToastRegionProps, useToastRegion } from '@react-aria/toast'
import type { ToastState } from '@react-stately/toast'
import { Toast } from './Toast'
import { useRef } from 'react'
import { NotificationType } from '../NotificationType'
import { ToastJoined } from './ToastJoined'
import { ToastData } from './ToastProvider'
import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
}
export function ToastRegion({ state, ...props }: ToastRegionProps) {
const ref = useRef(null)
const { regionProps } = useToastRegion(props, state, ref)
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => {
if (toast.content?.type === NotificationType.Joined) {
return <ToastJoined key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.Raised) {
return <ToastRaised key={toast.key} toast={toast} state={state} />
}
return <Toast key={toast.key} toast={toast} state={state} />
})}
</div>
)
}
@@ -1,18 +0,0 @@
import useSound from 'use-sound'
// fixme - handle dynamic audio output changes
export const useNotificationSound = () => {
const [play] = useSound('./sounds/notifications.mp3', {
sprite: {
joined: [0, 1150],
raised: [1400, 180],
message: [1580, 300],
waiting: [2039, 710],
success: [2740, 1304],
},
})
const triggerNotificationSound = (type: string) => {
play({ id: type })
}
return { triggerNotificationSound }
}
@@ -50,6 +50,7 @@ export const Conference = ({
} = useQuery({
queryKey: fetchKey,
staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation
enabled: !initialRoomData,
initialData: initialRoomData,
queryFn: () =>
fetchRoom({
@@ -4,20 +4,14 @@ import { Div, Button, type DialogProps, P } from '@/primitives'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { Heading, Dialog } from 'react-aria-components'
import { Text, text } from '@/primitives/Text'
import {
RiCheckLine,
RiCloseLine,
RiFileCopyLine,
RiSpam2Fill,
} from '@remixicon/react'
import { useEffect, useState } from 'react'
import { RiCloseLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
// fixme - extract in a proper primitive this dialog without overlay
const StyledRACDialog = styled(Dialog, {
base: {
position: 'fixed',
left: '0.75rem',
bottom: 80,
left: 30,
bottom: 90,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -40,25 +34,17 @@ export const InviteDialog = ({
const { t } = useTranslation('rooms')
const roomUrl = getRouteUrl('room', roomId)
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => setIsCopied(false), 3000)
return () => clearTimeout(timeout)
}
}, [isCopied])
const [isHovered, setIsHovered] = useState(false)
return (
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VStack
alignItems="left"
alignItems={'left'}
justify="start"
gap={0}
style={{ maxWidth: '100%', overflow: 'hidden' }}
style={{
maxWidth: '100%',
overflow: 'hidden',
}}
>
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
{t('shareDialog.heading')}
@@ -77,48 +63,36 @@ export const InviteDialog = ({
</Button>
</Div>
<P>{t('shareDialog.description')}</P>
<Button
variant={isCopied ? 'success' : 'primary'}
size="sm"
fullWidth
aria-label={t('shareDialog.copy')}
<HStack
justify={'space-between'}
alignItems="center"
style={{
justifyContent: 'start',
backgroundColor: '#f1f3f4',
borderRadius: '4px',
maxWidth: '100%',
}}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setIsCopied(true)
}}
onHoverChange={setIsHovered}
gap={0}
>
{isCopied ? (
<>
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
{t('shareDialog.copied')}
</>
) : (
<>
<RiFileCopyLine
size={18}
style={{ marginRight: '8px', minWidth: '18px' }}
/>
{isHovered ? (
t('shareDialog.copy')
) : (
<div
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
userSelect: 'none',
textWrap: 'nowrap',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
)}
</>
)}
</Button>
<div
style={{
paddingLeft: '0.75rem',
textOverflow: 'ellipsis',
overflow: 'hidden',
textWrap: 'nowrap',
userSelect: 'none',
}}
>
{roomUrl.replace(/^https?:\/\//, '')}
</div>
<Button
square
invisible
tooltip={t('shareDialog.copy')}
onPress={() => navigator.clipboard.writeText(roomUrl)}
>
<RiFileCopyLine size={24} />
</Button>
</HStack>
<HStack>
<div
style={{
@@ -1,33 +0,0 @@
import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
export const useLowerHandParticipant = () => {
const data = useRoomData()
const lowerHandParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newMetadata = JSON.parse(participant.metadata || '{}')
newMetadata.raised = !newMetadata.raised
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/UpdateParticipant'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
metadata: JSON.stringify(newMetadata),
permission: participant.permissions,
}),
}
)
}
return { lowerHandParticipant }
}
@@ -1,18 +0,0 @@
import { Participant } from 'livekit-client'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant'
export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant()
const lowerHandParticipants = (participants: Array<Participant>) => {
try {
const promises = participants.map((participant) =>
lowerHandParticipant(participant)
)
return Promise.all(promises)
} catch (error) {
throw new Error('An error occurred while lowering hands.')
}
}
return { lowerHandParticipants }
}
@@ -1,56 +0,0 @@
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
import { useParams } from 'wouter'
export const useRecordRoom = () => {
const data = useRoomData()
const { roomId: roomSlug } = useParams()
const recordRoom = () => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
if (!roomSlug) {
throw new Error('Room ID is not available')
}
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'/twirp/livekit.Egress/StartRoomCompositeEgress'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room_name: data.livekit.room,
audio_only: true,
file_outputs: [
{
file_extension: 'ogg',
filepath: `{room_name}_{time}_${roomSlug}`,
},
],
}),
}
)
}
const stopRecordingRoom = (egressId: string) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
return fetchServerApi(
buildServerApiUrl(data.livekit.url, '/twirp/livekit.Egress/StopEgress'),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
egressId,
}),
}
)
}
return { recordRoom, stopRecordingRoom }
}
@@ -1,26 +0,0 @@
import { useTrackMutedIndicator } from '@livekit/components-react'
import { Participant, Track } from 'livekit-client'
import Source = Track.Source
import { Div } from '@/primitives'
import { RiMicOffFill } from '@remixicon/react'
export const MutedMicIndicator = ({
participant,
}: {
participant: Participant
}) => {
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Source.Microphone,
})
if (!isMuted) {
return null
}
return (
<Div padding={0.25} backgroundColor="red" borderRadius="4px">
<RiMicOffFill size={16} color="white" />
</Div>
)
}
@@ -6,6 +6,7 @@ import {
ParticipantName,
ParticipantTileProps,
ScreenShareIcon,
TrackMutedIndicator,
useEnsureTrackRef,
useFeatureContext,
useIsEncrypted,
@@ -24,10 +25,6 @@ import {
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
import { RiHand } from '@remixicon/react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from '@/features/rooms/livekit/components/MutedMicIndicator'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -44,24 +41,19 @@ export function TrackRefContextIfNeeded(
)
}
interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean
}
export const ParticipantTile: (
props: ParticipantTileExtendedProps & React.RefAttributes<HTMLDivElement>
props: ParticipantTileProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
ParticipantTileExtendedProps
ParticipantTileProps
>(function ParticipantTile(
{
trackRef,
children,
onParticipantClick,
disableSpeakingIndicator,
disableMetadata,
...htmlProps
}: ParticipantTileExtendedProps,
}: ParticipantTileProps,
ref
) {
const trackReference = useEnsureTrackRef(trackRef)
@@ -92,10 +84,6 @@ export const ParticipantTile: (
[trackReference, layoutContext]
)
const { isHandRaised } = useRaisedHand({
participant: trackReference.participant,
})
return (
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
<TrackRefContextIfNeeded trackRef={trackReference}>
@@ -124,56 +112,34 @@ export const ParticipantTile: (
participant={trackReference.participant}
/>
</div>
{!disableMetadata && (
<div className="lk-participant-metadata">
<HStack gap={0.25}>
<MutedMicIndicator
participant={trackReference.participant}
/>
<div
className="lk-participant-metadata-item"
style={{
minHeight: '24px',
backgroundColor: isHandRaised ? 'white' : undefined,
color: isHandRaised ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{trackReference.source === Track.Source.Camera ? (
<>
{isHandRaised && (
<RiHand
color="black"
size={16}
style={{
marginInlineEnd: '.25rem', // fixme - match TrackMutedIndicator styling
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
)}
{isEncrypted && (
<LockLockedIcon
style={{ marginRight: '0.25rem' }}
/>
)}
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
<div className="lk-participant-metadata">
<div className="lk-participant-metadata-item">
{trackReference.source === Track.Source.Camera ? (
<>
{isEncrypted && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
)}
</div>
</HStack>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
<TrackMutedIndicator
trackRef={{
participant: trackReference.participant,
source: Track.Source.Microphone,
}}
show={'muted'}
></TrackMutedIndicator>
<ParticipantName />
</>
) : (
<>
<ScreenShareIcon style={{ marginRight: '0.25rem' }} />
<ParticipantName>&apos;s screen</ParticipantName>
</>
)}
</div>
)}
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
</>
)}
{!disableMetadata && <FocusToggle trackRef={trackReference} />}
<FocusToggle trackRef={trackReference} />
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
</div>
@@ -1,32 +0,0 @@
import { useRoomContext } from '@livekit/components-react'
import { useEffect, useState } from 'react'
import { RoomEvent } from 'livekit-client'
export const RecordingIndicator = () => {
const room = useRoomContext()
const [isRecording, setIsRecording] = useState(room.isRecording)
useEffect(() => {
const handleRecordingStatusChanges = (isRecording: boolean) => {
setIsRecording(isRecording)
}
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
return () => {
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanges)
}
}, [room])
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
width: '100%',
}}
>
Room is recording: {isRecording ? 'yes' : 'no'}
</div>
)
}
@@ -2,13 +2,18 @@ import { useTranslation } from 'react-i18next'
import { RiChat1Line } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useWidgetInteraction } from '../../hooks/useWidgetInteraction'
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
export const ChatToggle = () => {
const { t } = useTranslation('rooms')
const { isChatOpen, unreadMessages, toggleChat } = useWidgetInteraction()
const tooltipLabel = isChatOpen ? 'open' : 'closed'
const { dispatch, state } = useLayoutContext().widget
const tooltipLabel = state?.showChat ? 'open' : 'closed'
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
return (
<div
@@ -22,12 +27,15 @@ export const ChatToggle = () => {
legacyStyle
aria-label={t(`controls.chat.${tooltipLabel}`)}
tooltip={t(`controls.chat.${tooltipLabel}`)}
isSelected={isChatOpen}
onPress={() => toggleChat()}
isSelected={state?.showChat}
onPress={() => {
if (showParticipants) participantsStore.showParticipants = false
if (dispatch) dispatch({ msg: 'toggle_chat' })
}}
>
<RiChat1Line />
</ToggleButton>
{!!unreadMessages && (
{!!state?.unreadMessages && (
<div
className={css({
position: 'absolute',
@@ -1,54 +0,0 @@
import { useTranslation } from 'react-i18next'
import { RiHand } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { NotificationType } from '@/features/notifications/NotificationType'
export const HandToggle = () => {
const { t } = useTranslation('rooms')
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const label = isHandRaised
? t('controls.hand.lower')
: t('controls.hand.raise')
const notifyOtherParticipants = (isHandRaised: boolean) => {
room.localParticipant.publishData(
new TextEncoder().encode(
!isHandRaised ? NotificationType.Raised : NotificationType.Lowered
),
{
reliable: true,
}
)
}
return (
<div
className={css({
position: 'relative',
display: 'inline-block',
})}
>
<ToggleButton
square
legacyStyle
aria-label={label}
tooltip={label}
isSelected={isHandRaised}
onPress={() => {
notifyOtherParticipants(isHandRaised)
toggleRaisedHand()
}}
>
<RiHand />
</ToggleButton>
</div>
)
}
@@ -8,7 +8,6 @@ import { MenuItem, Menu as RACMenu } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction } from 'react'
import { DialogState } from '@/features/rooms/livekit/components/controls/Options/OptionsButton'
import { RecordingMenuItem } from './RecordingMenuItem.tsx'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = ({
@@ -48,7 +47,6 @@ export const OptionsMenuItems = ({
<RiSettings3Line size={18} />
{t('options.items.settings')}
</MenuItem>
<RecordingMenuItem />
</RACMenu>
)
}
@@ -1,51 +0,0 @@
import { MenuItem } from 'react-aria-components'
import { menuItemRecipe } from '@/primitives/menuItemRecipe.ts'
import { RiPauseCircleLine, RiRecordCircleLine } from '@remixicon/react'
import { useRecordRoom } from '@/features/rooms/livekit/api/recordRoom.ts'
import { useState } from 'react'
import { egressStore } from '@/stores/egress.tsx'
import { useSnapshot } from 'valtio'
export const RecordingMenuItem = () => {
const { recordRoom, stopRecordingRoom } = useRecordRoom()
const egressSnap = useSnapshot(egressStore)
const egressId = egressSnap.egressId
const [isPending, setIsPending] = useState(false)
const handleAction = async () => {
if (egressId) {
setIsPending(true)
const response = await stopRecordingRoom(egressId)
console.log(response)
egressStore.egressId = undefined
setIsPending(false)
} else {
setIsPending(true)
const response = await recordRoom()
egressStore.egressId = response['egress_id'] as string
setIsPending(false)
}
}
return (
<MenuItem
isDisabled={isPending}
className={menuItemRecipe({ icon: true })}
onAction={handleAction}
>
{egressId ? (
<>
<RiPauseCircleLine size={18} />
Stop recording room
</>
) : (
<>
<RiRecordCircleLine size={18} />
Record room
</>
)}
</MenuItem>
)
}
@@ -1,81 +0,0 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'
import { Button } from '@/primitives'
type HandRaisedListItemProps = {
participant: Participant
}
export const HandRaisedListItem = ({
participant,
}: HandRaisedListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
const { lowerHandParticipant } = useLowerHandParticipant()
return (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<Button
square
invisible
size="sm"
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
>
<RiHand />
</Button>
</HStack>
)
}
@@ -1,26 +0,0 @@
import { Button } from '@/primitives'
import { useLowerHandParticipants } from '@/features/rooms/livekit/api/lowerHandParticipants'
import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client'
type LowerAllHandsButtonProps = {
participants: Array<Participant>
}
export const LowerAllHandsButton = ({
participants,
}: LowerAllHandsButtonProps) => {
const { lowerHandParticipants } = useLowerHandParticipants()
const { t } = useTranslation('rooms')
return (
<Button
aria-label={t('participants.lowerParticipantsHand')}
size="sm"
fullWidth
variant="text"
onPress={() => lowerHandParticipants(participants)}
>
{t('participants.lowerParticipantsHand')}
</Button>
)
}
@@ -7,12 +7,15 @@ import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant, Track } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { Button as RACButton } from 'react-aria-components'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import {
useIsSpeaking,
useTrackMutedIndicator,
} from '@livekit/components-react'
import Source = Track.Source
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { RiMicOffLine } from '@remixicon/react'
import { TooltipWrapper } from '@/primitives/TooltipWrapper.tsx'
import { Button, Dialog, P } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
@@ -33,10 +36,10 @@ const MuteAlertDialog = ({
<Dialog isOpen={isOpen} role="alertdialog">
<P>{t('participants.muteParticipantAlert.description', { name })}</P>
<HStack gap={1}>
<Button variant="text" size="sm" onPress={onClose}>
<Button size="sm" invisible onPress={onClose}>
{t('participants.muteParticipantAlert.cancel')}
</Button>
<Button variant="text" size="sm" onPress={onSubmit}>
<Button size="sm" invisible onPress={onSubmit}>
{t('participants.muteParticipantAlert.confirm')}
</Button>
</HStack>
@@ -56,39 +59,45 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
source: Source.Microphone,
})
const isSpeaking = useIsSpeaking(participant)
const isDisabled = isLocal(participant) || (!isLocal(participant) && isMuted)
const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity
return (
<>
<Button
square
invisible
size="sm"
tooltip={
isLocal(participant)
? t('participants.muteYourself')
: t('participants.muteParticipant', {
name,
})
}
isDisabled={isMuted}
onPress={() =>
!isMuted && isLocal(participant)
? muteParticipant(participant)
: setIsAlertOpen(true)
}
<TooltipWrapper
tooltip={t('participants.muteParticipant', {
name,
})}
tooltipType="instant"
>
{isMuted ? (
<RiMicOffFill color={'gray'} />
) : (
<RiMicFill
style={{
animation: isSpeaking ? 'pulse_mic 800ms infinite' : undefined,
}}
/>
)}
</Button>
<RACButton
isDisabled={isDisabled}
className={css({
padding: '10px',
minWidth: '24px',
minHeight: '24px',
borderRadius: '50%',
backgroundColor: 'transparent',
transition: 'background 200ms',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
'&[data-focused]': {
backgroundColor: '#f5f5f5',
},
})}
onPress={() => !isMuted && setIsAlertOpen(true)}
>
{isMuted ? (
<RiMicOffLine color="gray" />
) : (
<ActiveSpeaker isSpeaking={isSpeaking} />
)}
</RACButton>
</TooltipWrapper>
<MuteAlertDialog
isOpen={isAlertOpen}
onSubmit={() =>
@@ -110,6 +119,7 @@ export const ParticipantListItem = ({
}: ParticipantListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity
return (
<HStack
role="listitem"
@@ -1,109 +0,0 @@
import { useState } from 'react'
import { css } from '@/styled-system/css'
import { ToggleButton } from 'react-aria-components'
import { HStack, styled, VStack } from '@/styled-system/jsx'
import { RiArrowUpSLine } from '@remixicon/react'
import { Participant } from 'livekit-client'
import { useTranslation } from 'react-i18next'
const ToggleHeader = styled(ToggleButton, {
base: {
minHeight: '40px', //fixme hardcoded value
paddingRight: '.5rem',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
alignItems: 'center',
transition: 'background 200ms',
borderTopRadius: '7px',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
},
})
const Container = styled('div', {
base: {
border: '1px solid #dadce0',
borderRadius: '8px',
margin: '0 .625rem',
},
})
const ListContainer = styled(VStack, {
base: {
borderTop: '1px solid #dadce0',
alignItems: 'start',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
paddingY: '0.5rem',
paddingX: '1rem',
gap: 0,
},
})
type ParticipantsCollapsableListProps = {
heading: string
participants: Array<Participant>
renderParticipant: (participant: Participant) => JSX.Element
action?: () => JSX.Element
}
export const ParticipantsCollapsableList = ({
heading,
participants,
renderParticipant,
action,
}: ParticipantsCollapsableListProps) => {
const { t } = useTranslation('rooms')
const [isOpen, setIsOpen] = useState(true)
const label = t(`participants.collapsable.${isOpen ? 'close' : 'open'}`, {
name: heading,
})
return (
<Container>
<ToggleHeader
isSelected={isOpen}
aria-label={label}
onPress={() => setIsOpen(!isOpen)}
style={{
borderRadius: !isOpen ? '7px' : undefined,
}}
>
<HStack
justify="space-between"
className={css({
margin: '0 1.25rem',
width: '100%',
})}
>
<div
className={css({
fontSize: '1rem',
})}
>
{heading}
</div>
<div>{participants?.length || 0}</div>
</HStack>
<RiArrowUpSLine
size={32}
style={{
transform: isOpen ? 'rotate(-180deg)' : undefined,
transition: 'transform 200ms',
}}
/>
</ToggleHeader>
{isOpen && (
<ListContainer>
{action && action()}
{participants.map((participant) => renderParticipant(participant))}
</ListContainer>
)}
</Container>
)
}
@@ -2,16 +2,14 @@ import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { Heading } from 'react-aria-components'
import { Box, Button, Div, H } from '@/primitives'
import { Box, Button, Div } from '@/primitives'
import { text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
import { participantsStore } from '@/stores/participants'
import { useTranslation } from 'react-i18next'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton'
import { VStack } from '@/styled-system/jsx'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
@@ -37,35 +35,29 @@ export const ParticipantsList = () => {
...sortedRemoteParticipants,
]
const raisedHandParticipants = participants.filter((participant) => {
const data = JSON.parse(participant.metadata || '{}')
return data.raised
})
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
<Box
size="sm"
minWidth="360px"
minWidth="300px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0,
gap: 0,
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
>
{t('participants.heading')}
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
<span>{t('participants.heading')}</span>{' '}
<span
className={css({
marginLeft: '0.75rem',
fontWeight: 'normal',
fontSize: '1rem',
})}
>
{participants?.length}
</span>
</Heading>
<Div position="absolute" top="5" right="5">
<Button
@@ -78,41 +70,24 @@ export const ParticipantsList = () => {
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">
<H
lvl={2}
{sortedParticipants?.length > 0 && (
<VStack
role="list"
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
marginBottom: '0.83em',
alignItems: 'start',
gap: 'none',
overflowY: 'scroll',
overflowX: 'hidden',
minHeight: 0,
flexGrow: 1,
display: 'flex',
})}
>
{t('participants.subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<Div marginBottom=".9375rem">
<ParticipantsCollapsableList
heading={t('participants.raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
action={() => (
<LowerAllHandsButton participants={raisedHandParticipants} />
)}
/>
</Div>
)}
<ParticipantsCollapsableList
heading={t('participants.contributors')}
participants={sortedParticipants}
renderParticipant={(participant) => (
{sortedParticipants.map((participant) => (
<ParticipantListItem participant={participant} />
)}
/>
</Div>
))}
</VStack>
)}
</Box>
)
}
@@ -2,12 +2,15 @@ import { useTranslation } from 'react-i18next'
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { useWidgetInteraction } from '../../../hooks/useWidgetInteraction'
import { useLayoutContext, useParticipants } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
export const ParticipantsToggle = () => {
const { t } = useTranslation('rooms')
const { dispatch, state } = useLayoutContext().widget
/**
* Context could not be used due to inconsistent refresh behavior.
* The 'numParticipant' property on the room only updates when the room's metadata changes,
@@ -16,9 +19,10 @@ export const ParticipantsToggle = () => {
const participants = useParticipants()
const numParticipants = participants?.length
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
const participantsSnap = useSnapshot(participantsStore)
const showParticipants = participantsSnap.showParticipants
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
const tooltipLabel = showParticipants ? 'open' : 'closed'
return (
<div
@@ -32,8 +36,11 @@ export const ParticipantsToggle = () => {
legacyStyle
aria-label={t(`controls.participants.${tooltipLabel}`)}
tooltip={t(`controls.participants.${tooltipLabel}`)}
isSelected={isParticipantsOpen}
onPress={() => toggleParticipants()}
isSelected={showParticipants}
onPress={() => {
if (dispatch && state?.showChat) dispatch({ msg: 'toggle_chat' })
participantsStore.showParticipants = !showParticipants
}}
>
<RiGroupLine />
</ToggleButton>
@@ -1,22 +0,0 @@
import { LocalParticipant, Participant } from 'livekit-client'
import { useParticipantInfo } from '@livekit/components-react'
import { isLocal } from '@/utils/livekit'
type useRaisedHandProps = {
participant: Participant
}
export function useRaisedHand({ participant }: useRaisedHandProps) {
const { metadata } = useParticipantInfo({ participant })
const parsedMetadata = JSON.parse(metadata || '{}')
const toggleRaisedHand = () => {
if (isLocal(participant)) {
parsedMetadata.raised = !parsedMetadata.raised
const localParticipant = participant as LocalParticipant
localParticipant.setMetadata(JSON.stringify(parsedMetadata))
}
}
return { isHandRaised: parsedMetadata.raised, toggleRaisedHand }
}
@@ -1,34 +0,0 @@
import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants.ts'
export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget
const participantsSnap = useSnapshot(participantsStore)
const isParticipantsOpen = participantsSnap.showParticipants
const toggleParticipants = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
participantsStore.showParticipants = !isParticipantsOpen
}
const toggleChat = () => {
if (isParticipantsOpen) {
participantsStore.showParticipants = false
}
if (dispatch) {
dispatch({ msg: 'toggle_chat' })
}
}
return {
toggleParticipants,
toggleChat,
isChatOpen: state?.showChat,
unreadMessages: state?.unreadMessages,
isParticipantsOpen,
}
}
@@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle'
import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle'
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
/** @public */
export type ControlBarControls = {
@@ -184,7 +183,6 @@ export function ControlBar({
)}
</TrackToggle>
)}
<HandToggle />
<ChatToggle />
<ParticipantsToggle />
<OptionsButton />
@@ -35,8 +35,6 @@ import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants'
import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
import { RecordingIndicator } from '@/features/rooms/livekit/components/RecordingIndicator.tsx'
const LayoutWrapper = styled(
'div',
@@ -185,40 +183,26 @@ export function VideoConference({
onWidgetChange={widgetUpdate}
>
<div className="lk-video-conference-inner">
<RecordingIndicator />
<LayoutWrapper>
<div
style={{ display: 'flex', position: 'relative', width: '100%' }}
>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
{!focusTrack ? (
<div
className="lk-grid-layout-wrapper"
style={{ height: 'auto' }}
>
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div className="lk-focus-layout-wrapper">
<FocusLayoutContainer>
<CarouselLayout tracks={carouselTracks}>
<ParticipantTile />
</GridLayout>
</div>
) : (
<div
className="lk-focus-layout-wrapper"
style={{ height: 'auto' }}
>
<FocusLayoutContainer>
<CarouselLayout
tracks={carouselTracks}
style={{
minWidth: '200px',
}}
>
<ParticipantTile />
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
<MainNotificationToast />
</div>
</CarouselLayout>
{focusTrack && <FocusLayout trackRef={focusTrack} />}
</FocusLayoutContainer>
</div>
)}
<Chat
style={{ display: widgetState.showChat ? 'grid' : 'none' }}
messageFormatter={chatMessageFormatter}
@@ -12,8 +12,6 @@ import {
import { AccountTab } from './tabs/AccountTab'
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useRef } from 'react'
const tabsStyle = css({
maxHeight: '40.625rem', // fixme size copied from meet settings modal
@@ -30,13 +28,13 @@ const tabListContainerStyle = css({
borderRight: '1px solid lightGray', // fixme poor color management
paddingY: '1rem',
paddingRight: '1.5rem',
flex: '0 0 16rem', // fixme size copied from meet settings modal
})
const tabPanelContainerStyle = css({
display: 'flex',
flexGrow: '1',
marginTop: '3.5rem',
minWidth: 0,
})
export type SettingsDialogExtended = Pick<
@@ -47,39 +45,25 @@ export type SettingsDialogExtended = Pick<
export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
// display only icon on small screen
const { t } = useTranslation('settings')
const dialogEl = useRef<HTMLDivElement>(null)
const { width } = useSize(dialogEl)
const isWideScreen = !width || width >= 800 // fixme - hardcoded 50rem in pixel
return (
<Dialog innerRef={dialogEl} {...props} role="dialog" type="flex">
<Dialog {...props} role="dialog" type="flex">
<Tabs orientation="vertical" className={tabsStyle}>
<div
className={tabListContainerStyle}
style={{
flex: isWideScreen ? '0 0 16rem' : undefined,
paddingTop: !isWideScreen ? '64px' : undefined,
paddingRight: !isWideScreen ? '1rem' : undefined,
}}
>
{isWideScreen && (
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
)}
<div className={tabListContainerStyle}>
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{t('dialog.heading')}
</Heading>
<TabList border={false} aria-label="Chat log orientation example">
<Tab icon highlight id="1">
<RiAccountCircleLine />
{isWideScreen && t('tabs.account')}
{t('tabs.account')}
</Tab>
<Tab icon highlight id="2">
<RiSpeakerLine />
{isWideScreen && t('tabs.audio')}
{t('tabs.audio')}
</Tab>
<Tab icon highlight id="3">
<RiSettings3Line />
{isWideScreen && t('tabs.general')}
{t('tabs.general')}
</Tab>
</TabList>
</div>
@@ -1,55 +1,10 @@
import { DialogProps, Field, H } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import {
useIsSpeaking,
useMediaDeviceSelect,
useRoomContext,
} from '@livekit/components-react'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { isSafari } from '@/utils/livekit'
import { useTranslation } from 'react-i18next'
import { SoundTester } from '@/components/SoundTester'
import { HStack } from '@/styled-system/jsx'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import { ReactNode } from 'react'
type RowWrapperProps = {
heading: string
children: ReactNode[]
}
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
return (
<>
<H lvl={2}>{heading}</H>
<HStack
gap={0}
style={{
flexWrap: 'wrap',
}}
>
<div
style={{
flex: '1 1 215px',
minWidth: 0,
}}
>
{children[0]}
</div>
<div
style={{
width: '10rem',
justifyContent: 'center',
display: 'flex',
paddingLeft: '1.5rem',
}}
>
{children[1]}
</div>
</HStack>
</>
)
}
import { SoundTester } from '@/components/SoundTester.tsx'
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
@@ -58,9 +13,6 @@ type DeviceItems = Array<{ value: string; label: string }>
export const AudioTab = ({ id }: AudioTabProps) => {
const { t } = useTranslation('settings')
const { localParticipant } = useRoomContext()
const isSpeaking = useIsSpeaking(localParticipant)
const {
devices: devicesOut,
@@ -108,32 +60,20 @@ export const AudioTab = ({ id }: AudioTabProps) => {
return (
<TabPanel padding={'md'} flex id={id}>
<RowWrapper heading={t('audio.microphone.heading')}>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={
activeDeviceIdIn || getDefaultSelectedKey(itemsIn)
}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
style={{
width: '100%',
}}
/>
<>
{localParticipant.isMicrophoneEnabled ? (
<ActiveSpeaker isSpeaking={isSpeaking} />
) : (
<span>Micro désactivé</span>
)}
</>
</RowWrapper>
<H lvl={2}>{t('audio.microphone.heading')}</H>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={activeDeviceIdIn || getDefaultSelectedKey(itemsIn)}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
/>
{/* Safari has a known limitation where its implementation of 'enumerateDevices' does not include audio output devices.
To prevent errors or an empty selection list, we only render the speakers selection field on non-Safari browsers. */}
{!isSafari() && (
<RowWrapper heading={t('audio.speakers.heading')}>
<>
<H lvl={2}>{t('audio.speakers.heading')}</H>
<Field
type="select"
label={t('audio.speakers.label')}
@@ -145,12 +85,9 @@ export const AudioTab = ({ id }: AudioTabProps) => {
setActiveMediaDeviceOut(key as string)
}
{...disabledProps}
style={{
minWidth: 0,
}}
/>
<SoundTester />
</RowWrapper>
</>
)}
</TabPanel>
)
+2 -2
View File
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "",
"description": "",
"copy": "",
"copied": "",
"permissions": ""
"copy": "",
"inputLabel": ""
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "",
"joined": {
"description": ""
},
"raised": {
"description": "",
"cta": ""
}
}
+1 -16
View File
@@ -13,7 +13,6 @@
"leaveRoomPrompt": "",
"shareDialog": {
"copy": "",
"copied": "",
"heading": "",
"description": "",
"permissions": ""
@@ -33,10 +32,6 @@
"open": "",
"closed": ""
},
"hand": {
"raise": "",
"lower": ""
},
"leave": "",
"participants": {
"open": "",
@@ -54,23 +49,13 @@
},
"participants": {
"heading": "",
"subheading": "",
"closeButton": "",
"contributors": "",
"collapsable": {
"open": "",
"close": ""
},
"you": "",
"muteYourself": "",
"muteParticipant": "",
"muteParticipantAlert": {
"description": "",
"confirm": "",
"cancel": ""
},
"raisedHands": "",
"lowerParticipantHand": "",
"lowerParticipantsHand": ""
}
}
}
+3 -3
View File
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Your connection details",
"description": "Send this link to the people you want to invite to the meeting. They will be able to join without Agent Connect.",
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"permissions": "People with this link do not need your permission to join this meeting."
"copied": "Copied",
"copy": "Copy",
"inputLabel": "Meeting link"
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "A contributor",
"joined": {
"description": "{{name}} has joined the room"
},
"raised": {
"description": "{{name}} has raised their hand.",
"cta": "Open waiting list"
}
}
+3 -18
View File
@@ -12,8 +12,7 @@
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
"copy": "Copy the meeting link",
"copied": "Link copied to clipboard",
"copy": "Copy link",
"heading": "Your meeting is ready",
"description": "Share this link with people you want to invite to the meeting.",
"permissions": "People with this link do not need your permission to join this meeting."
@@ -33,10 +32,6 @@
"open": "Close the chat",
"closed": "Open the chat"
},
"hand": {
"raise": "Raise hand",
"lower": "Lower hand"
},
"leave": "Leave",
"participants": {
"open": "Hide everyone",
@@ -54,23 +49,13 @@
},
"participants": {
"heading": "Participants",
"subheading": "In room",
"closeButton": "Hide participants",
"you": "You",
"contributors": "Contributors",
"collapsable": {
"open": "Open {{name}} list",
"close": "Close {{name}} list"
},
"muteYourself": "Close your mic",
"muteParticipant": "Close the mic of {{name}}",
"muteParticipantAlert": {
"description": "Mute {{name}} for all participants? {{name}} will be the only one who can unmute themselves.",
"description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.",
"confirm": "Mute",
"cancel": "Cancel"
},
"raisedHands": "Raised hands",
"lowerParticipantHand": "Lower {{name}}'s hand",
"lowerParticipantsHand": "Lower all hands"
}
}
}
+3 -3
View File
@@ -17,8 +17,8 @@
"laterMeetingDialog": {
"heading": "Vos informations de connexion",
"description": "Envoyez ce lien aux personnes que vous souhaitez inviter à la réunion. Ils pourront la rejoindre sans Agent Connect.",
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
"copied": "Lien copié",
"copy": "Copier le lien",
"inputLabel": "Lien vers la réunion"
}
}
@@ -1,10 +0,0 @@
{
"defaultName": "Un contributeur",
"joined": {
"description": "{{name}} participe à l'appel"
},
"raised": {
"description": "{{name}} a levé la main.",
"cta": "Ouvrir la file d'attente"
}
}
+2 -17
View File
@@ -12,8 +12,7 @@
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
"copy": "Copier le lien de la réunion",
"copied": "Lien copié dans le presse-papiers",
"copy": "Copier le lien",
"heading": "Votre réunion est prête",
"description": "Partagez ce lien avec les personnes que vous souhaitez inviter à la réunion.",
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
@@ -33,10 +32,6 @@
"open": "Masquer le chat",
"closed": "Afficher le chat"
},
"hand": {
"raise": "Lever la main",
"lower": "Baisser la main"
},
"leave": "Quitter",
"participants": {
"open": "Masquer les participants",
@@ -54,23 +49,13 @@
},
"participants": {
"heading": "Participants",
"subheading": "Dans la réunion",
"closeButton": "Masquer les participants",
"you": "Vous",
"contributors": "Contributeurs",
"collapsable": {
"open": "Ouvrir la liste {{name}}",
"close": "Fermer la liste {{name}}"
},
"muteYourself": "Couper votre micro",
"muteParticipant": "Couper le micro de {{name}}",
"muteParticipantAlert": {
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
"confirm": "Couper le micro",
"cancel": "Annuler"
},
"raisedHands": "Mains levées",
"lowerParticipantHand": "Baisser la main de {{name}}",
"lowerParticipantsHand": "Baisser la main de tous les participants"
}
}
}
+1 -4
View File
@@ -10,7 +10,6 @@ import {
} from 'react-aria-components'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
import { text } from './Text'
import { MutableRefObject } from 'react'
const StyledModalOverlay = styled(ModalOverlay, {
base: {
@@ -62,7 +61,6 @@ export type DialogProps = RACDialogProps & {
*/
onOpenChange?: (isOpen: boolean) => void
type?: 'flex'
innerRef?: MutableRefObject<HTMLDivElement | null>
}
export const Dialog = ({
@@ -71,7 +69,6 @@ export const Dialog = ({
onClose,
isOpen,
onOpenChange,
innerRef,
...dialogProps
}: DialogProps) => {
const isAlert = dialogProps['role'] === 'alertdialog'
@@ -96,7 +93,7 @@ export const Dialog = ({
<VerticallyOffCenter>
<Div margin="auto" width="fit-content" maxWidth="full">
<Div margin="1rem" pointerEvents="auto">
<Box size="sm" type={boxType} ref={innerRef}>
<Box size="sm" type={boxType}>
{!!title && (
<Heading
slot="title"
-1
View File
@@ -23,7 +23,6 @@ import { Div } from './Div'
const FieldWrapper = styled('div', {
base: {
marginBottom: 'textfield',
minWidth: 0,
},
})
-3
View File
@@ -43,9 +43,6 @@ const StyledButton = styled(Button, {
const StyledSelectValue = styled(SelectValue, {
base: {
textOverflow: 'ellipsis',
overflow: 'hidden',
textWrap: 'nowrap',
'&[data-placeholder]': {
color: 'default.subtle-text',
fontStyle: 'italic',
@@ -25,7 +25,7 @@ export const TooltipWrapper = ({
children: ReactNode
} & TooltipWrapperProps) => {
return tooltip ? (
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
+3 -16
View File
@@ -23,9 +23,6 @@ export const buttonRecipe = cva({
'&[data-selected]': {
backgroundColor: 'colorPalette.active',
},
'&[data-disabled]': {
cursor: 'auto',
},
},
variants: {
size: {
@@ -73,13 +70,6 @@ export const buttonRecipe = cva({
backgroundColor: 'success.subtle!',
},
},
text: {
color: 'primary',
'&[data-hovered]': {
background: 'gray.100 !important',
color: 'primary !important',
},
},
danger: {
colorPalette: 'danger',
borderColor: 'danger.600',
@@ -150,7 +140,6 @@ export const buttonRecipe = cva({
legacyStyle: {
true: {
borderColor: 'gray.400',
transition: 'border 200ms, background 200ms, color 200ms',
'&[data-hovered]': {
borderColor: 'gray.500',
},
@@ -158,12 +147,10 @@ export const buttonRecipe = cva({
borderColor: 'gray.500',
},
'&[data-selected]': {
backgroundColor: '#1d4ed8',
color: 'white',
borderColor: 'gray.500',
background: '#e5e7eb',
borderColor: 'gray.400',
'&[data-hovered]': {
borderColor: '#6b7280',
backgroundColor: '#1e40af',
backgroundColor: 'gray.300',
},
},
},
-9
View File
@@ -1,9 +0,0 @@
import { proxy } from 'valtio'
type State = {
egressId: string | undefined
}
export const egressStore = proxy<State>({
egressId: undefined,
})
+1 -1
View File
@@ -24,7 +24,7 @@ backend:
OIDC_RP_CLIENT_ID: {{ .Values.oidc.clientId }}
OIDC_RP_CLIENT_SECRET: {{ .Values.oidc.clientSecret }}
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_RP_SCOPES: "openid email groups"
OIDC_REDIRECT_ALLOWED_HOSTS: https://meet.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://meet.127.0.0.1.nip.io
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.5"
tag: "v0.1.4"
backend:
migrateJobAnnotations:
@@ -108,7 +108,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.5"
tag: "v0.1.4"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "v0.1.5"
tag: "v0.1.4"
backend:
migrateJobAnnotations:
@@ -109,7 +109,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "v0.1.5"
tag: "v0.1.4"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/meet-backend
pullPolicy: Always
tag: "hackathon"
tag: "main"
backend:
migrateJobAnnotations:
@@ -108,7 +108,7 @@ frontend:
image:
repository: lasuite/meet-frontend
pullPolicy: Always
tag: "hackathon"
tag: "main"
ingress:
enabled: true
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.5",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.5",
"version": "0.1.4",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.5",
"version": "0.1.4",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {