Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14885e0ffa | |||
| 054e7ba945 | |||
| 25506b6a2a | |||
| 0498a9e7f9 | |||
| 0a63270154 | |||
| b962dddbf2 | |||
| 16929bcc83 | |||
| 354bdd8bfe | |||
| ccca2b9472 |
@@ -117,8 +117,7 @@ jobs:
|
||||
- build-and-push-backend
|
||||
- build-and-push-summary
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
@@ -126,6 +125,6 @@ jobs:
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/numerique-gouv/lasuite-deploiement"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
|
||||
load('ext://namespace', 'namespace_create', 'namespace_inject')
|
||||
namespace_create('meet')
|
||||
|
||||
def clean_old_images(image_name):
|
||||
local('docker images -q %s | tail -n +2 | xargs -r docker rmi' % image_name)
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/meet-backend:latest',
|
||||
context='..',
|
||||
@@ -15,6 +19,7 @@ docker_build(
|
||||
)
|
||||
]
|
||||
)
|
||||
clean_old_images('localhost:5001/meet-backend')
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/meet-frontend:latest',
|
||||
@@ -26,6 +31,7 @@ docker_build(
|
||||
sync('../src/frontend', '/home/frontend'),
|
||||
]
|
||||
)
|
||||
clean_old_images('localhost:5001/meet-frontend')
|
||||
|
||||
docker_build(
|
||||
'localhost:5001/meet-summary:latest',
|
||||
@@ -37,6 +43,7 @@ docker_build(
|
||||
sync('../src/summary', '/home/summary'),
|
||||
]
|
||||
)
|
||||
clean_old_images('localhost:5001/meet-summary')
|
||||
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .'))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.8.1",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
|
||||
import { ToastProvider, toastQueue } from './components/ToastProvider'
|
||||
@@ -7,11 +7,20 @@ import { NotificationDuration } from './NotificationDuration'
|
||||
import { Div } from '@/primitives'
|
||||
import { ChatMessage, isMobileBrowser } from '@livekit/components-core'
|
||||
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
|
||||
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
|
||||
import {
|
||||
ANIMATION_DURATION,
|
||||
ReactionPortals,
|
||||
} from '@/features/rooms/livekit/components/ReactionPortal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const MainNotificationToast = () => {
|
||||
const room = useRoomContext()
|
||||
const { triggerNotificationSound } = useNotificationSound()
|
||||
|
||||
const [reactions, setReactions] = useState<Reaction[]>([])
|
||||
const instanceIdRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
const handleChatMessage = (
|
||||
chatMessage: ChatMessage,
|
||||
@@ -34,13 +43,31 @@ export const MainNotificationToast = () => {
|
||||
}
|
||||
}, [room, triggerNotificationSound])
|
||||
|
||||
const handleEmoji = (emoji: string, participant: Participant) => {
|
||||
if (!emoji) return
|
||||
const id = instanceIdRef.current++
|
||||
setReactions((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id,
|
||||
emoji,
|
||||
participant,
|
||||
},
|
||||
])
|
||||
setTimeout(() => {
|
||||
setReactions((prev) => prev.filter((instance) => instance.id !== id))
|
||||
}, ANIMATION_DURATION)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleDataReceived = (
|
||||
payload: Uint8Array,
|
||||
participant?: RemoteParticipant
|
||||
) => {
|
||||
const decoder = new TextDecoder()
|
||||
const notificationType = decoder.decode(payload)
|
||||
const notificationPayload = JSON.parse(decoder.decode(payload))
|
||||
const notificationType = notificationPayload.type
|
||||
const data = notificationPayload.data
|
||||
|
||||
if (!participant) return
|
||||
|
||||
@@ -54,6 +81,9 @@ export const MainNotificationToast = () => {
|
||||
{ timeout: NotificationDuration.ALERT }
|
||||
)
|
||||
break
|
||||
case NotificationType.ReactionReceived:
|
||||
handleEmoji(data?.emoji, participant)
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -157,9 +187,14 @@ export const MainNotificationToast = () => {
|
||||
}
|
||||
}, [room])
|
||||
|
||||
// Without this line, when the component first renders,
|
||||
// the 'notifications' namespace might not be loaded yet
|
||||
useTranslation(['notifications'])
|
||||
|
||||
return (
|
||||
<Div position="absolute" bottom={0} right={5} zIndex={1000}>
|
||||
<ToastProvider />
|
||||
<ReactionPortals reactions={reactions} />
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ export const NotificationDuration = {
|
||||
PARTICIPANT_JOINED: ToastDuration.LONG,
|
||||
HAND_RAISED: ToastDuration.LONG,
|
||||
LOWER_HAND: ToastDuration.EXTRA_LONG,
|
||||
REACTION_RECEIVED: ToastDuration.SHORT,
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NotificationType } from './NotificationType'
|
||||
|
||||
export interface NotificationPayload {
|
||||
type: NotificationType
|
||||
data?: {
|
||||
emoji?: string
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,5 @@ export enum NotificationType {
|
||||
ParticipantMuted = 'participantMuted',
|
||||
MessageReceived = 'messageReceived',
|
||||
LowerHand = 'lowerHand',
|
||||
ReactionReceived = 'reactionReceived',
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildServerApiUrl } from './buildServerApiUrl'
|
||||
import { useRoomData } from '../hooks/useRoomData'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
||||
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
|
||||
|
||||
export const useMuteParticipant = () => {
|
||||
const data = useRoomData()
|
||||
@@ -12,7 +13,10 @@ export const useMuteParticipant = () => {
|
||||
|
||||
const notifyParticipant = async (participant: Participant) => {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(NotificationType.ParticipantMuted)
|
||||
const payload: NotificationPayload = {
|
||||
type: NotificationType.ParticipantMuted,
|
||||
}
|
||||
const data = encoder.encode(JSON.stringify(payload))
|
||||
await room.localParticipant.publishData(data, {
|
||||
reliable: true,
|
||||
destinationIdentities: [participant.identity],
|
||||
|
||||
@@ -20,14 +20,41 @@ export const FullScreenShareWarning = ({
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const screenSharePreferences = useSnapshot(ScreenSharePreferenceStore)
|
||||
|
||||
const isFullScreenCapture = useMemo(() => {
|
||||
const trackLabel =
|
||||
trackReference.publication?.track?.mediaStreamTrack?.label
|
||||
return trackLabel?.includes('screen')
|
||||
const isFullScreenSharing = useMemo(() => {
|
||||
if (trackReference?.source !== 'screen_share') return false
|
||||
|
||||
const mediaStreamTrack = trackReference.publication?.track?.mediaStreamTrack
|
||||
|
||||
if (!mediaStreamTrack) return false
|
||||
|
||||
// Method 1: Display Surface Check (Chrome & Edge)
|
||||
const trackSettings = mediaStreamTrack.getSettings()
|
||||
if (trackSettings?.displaySurface) {
|
||||
return trackSettings.displaySurface === 'monitor'
|
||||
}
|
||||
|
||||
// Method 2: Track Constraints Check
|
||||
const constraints = mediaStreamTrack.getConstraints()
|
||||
if (constraints?.displaySurface) {
|
||||
return constraints.displaySurface === 'monitor'
|
||||
}
|
||||
|
||||
// Method 3: Label Analysis (Firefox, Safari)
|
||||
const trackLabel = mediaStreamTrack.label.toLowerCase()
|
||||
const fullScreenIndicators = [
|
||||
'monitor',
|
||||
'screen',
|
||||
'display',
|
||||
'entire screen',
|
||||
'full screen',
|
||||
]
|
||||
return fullScreenIndicators.some((indicator) =>
|
||||
trackLabel.includes(indicator)
|
||||
)
|
||||
}, [trackReference])
|
||||
|
||||
const shouldShowWarning =
|
||||
screenSharePreferences.enabled && isFullScreenCapture
|
||||
screenSharePreferences.enabled && isFullScreenSharing
|
||||
|
||||
const handleStopScreenShare = async () => {
|
||||
if (!localParticipant.isScreenShareEnabled) return
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Text } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Participant } from 'livekit-client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
|
||||
|
||||
export const ANIMATION_DURATION = 3000
|
||||
export const ANIMATION_DISTANCE = 300
|
||||
export const FADE_OUT_THRESHOLD = 0.7
|
||||
export const REACTION_SPAWN_WIDTH_RATIO = 0.2
|
||||
export const INITIAL_POSITION = 200
|
||||
|
||||
interface FloatingReactionProps {
|
||||
emoji: string
|
||||
name?: string
|
||||
speed?: number
|
||||
scale?: number
|
||||
}
|
||||
|
||||
export function FloatingReaction({
|
||||
emoji,
|
||||
name,
|
||||
speed = 1,
|
||||
scale = 1,
|
||||
}: FloatingReactionProps) {
|
||||
const [deltaY, setDeltaY] = useState(0)
|
||||
const [opacity, setOpacity] = useState(1)
|
||||
|
||||
const left = useMemo(
|
||||
() => Math.random() * window.innerWidth * REACTION_SPAWN_WIDTH_RATIO,
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let start: number | null = null
|
||||
function animate(timestamp: number) {
|
||||
if (start === null) start = timestamp
|
||||
const elapsed = timestamp - start
|
||||
if (elapsed < 0) {
|
||||
setOpacity(0)
|
||||
} else {
|
||||
const progress = Math.min(elapsed / ANIMATION_DURATION, 1)
|
||||
const distance = ANIMATION_DISTANCE * speed
|
||||
const newY = progress * distance
|
||||
setDeltaY(newY)
|
||||
if (progress > FADE_OUT_THRESHOLD) {
|
||||
setOpacity(1 - (progress - FADE_OUT_THRESHOLD) / 0.3)
|
||||
}
|
||||
}
|
||||
if (elapsed < ANIMATION_DURATION) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
const req = requestAnimationFrame(animate)
|
||||
return () => cancelAnimationFrame(req)
|
||||
}, [speed])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3rem',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
style={{
|
||||
left: left,
|
||||
bottom: INITIAL_POSITION + deltaY,
|
||||
transform: `scale(${scale})`,
|
||||
opacity: opacity,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
lineHeight: '45px',
|
||||
})}
|
||||
>
|
||||
{emoji}
|
||||
</span>
|
||||
{name && (
|
||||
<Text
|
||||
variant="sm"
|
||||
className={css({
|
||||
backgroundColor: 'primaryDark.100',
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
borderRadius: '20px',
|
||||
paddingX: '0.5rem',
|
||||
paddingY: '0.15rem',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
lineHeight: '14px',
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ReactionPortal({
|
||||
emoji,
|
||||
participant,
|
||||
}: {
|
||||
emoji: string
|
||||
participant: Participant
|
||||
}) {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
||||
const speed = useMemo(() => Math.random() * 1.5 + 0.5, [])
|
||||
const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), [])
|
||||
return createPortal(
|
||||
<div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
})}
|
||||
>
|
||||
<FloatingReaction
|
||||
emoji={emoji}
|
||||
speed={speed}
|
||||
scale={scale}
|
||||
name={participant?.isLocal ? t('you') : participant.name}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) =>
|
||||
reactions.map((instance) => (
|
||||
<ReactionPortal
|
||||
key={instance.id}
|
||||
emoji={instance.emoji}
|
||||
participant={instance.participant}
|
||||
/>
|
||||
))
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEmotionLine } from '@remixicon/react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { Menu, ToggleButton, Button } from '@/primitives'
|
||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
||||
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
|
||||
import {
|
||||
ANIMATION_DURATION,
|
||||
ReactionPortals,
|
||||
} from '@/features/rooms/livekit/components/ReactionPortal'
|
||||
import { Toolbar as RACToolbar } from 'react-aria-components'
|
||||
import { Participant } from 'livekit-client'
|
||||
|
||||
const EMOJIS = ['👍', '👎', '👏', '❤️', '😂', '😮', '🎉']
|
||||
|
||||
export interface Reaction {
|
||||
id: number
|
||||
emoji: string
|
||||
participant: Participant
|
||||
}
|
||||
|
||||
export const ReactionsToggle = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
|
||||
const [reactions, setReactions] = useState<Reaction[]>([])
|
||||
const instanceIdRef = useRef(0)
|
||||
const room = useRoomContext()
|
||||
|
||||
const sendReaction = async (emoji: string) => {
|
||||
const encoder = new TextEncoder()
|
||||
const payload: NotificationPayload = {
|
||||
type: NotificationType.ReactionReceived,
|
||||
data: {
|
||||
emoji: emoji,
|
||||
},
|
||||
}
|
||||
const data = encoder.encode(JSON.stringify(payload))
|
||||
await room.localParticipant.publishData(data, { reliable: true })
|
||||
|
||||
const newReaction = {
|
||||
id: instanceIdRef.current++,
|
||||
emoji,
|
||||
participant: room.localParticipant,
|
||||
}
|
||||
setReactions((prev) => [...prev, newReaction])
|
||||
|
||||
// Remove this reaction after animation
|
||||
setTimeout(() => {
|
||||
setReactions((prev) =>
|
||||
prev.filter((instance) => instance.id !== newReaction.id)
|
||||
)
|
||||
}, ANIMATION_DURATION)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu variant="dark" placement="top">
|
||||
<ToggleButton
|
||||
square
|
||||
variant="primaryDark"
|
||||
aria-label={t('button')}
|
||||
tooltip={t('button')}
|
||||
>
|
||||
<RiEmotionLine />
|
||||
</ToggleButton>
|
||||
<RACToolbar
|
||||
className={css({
|
||||
display: 'flex',
|
||||
})}
|
||||
>
|
||||
{EMOJIS.map((emoji, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onPress={() => sendReaction(emoji)}
|
||||
aria-label={t('send', { emoji })}
|
||||
variant="quaternaryText"
|
||||
size="sm"
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: '20px',
|
||||
})}
|
||||
>
|
||||
{emoji}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</RACToolbar>
|
||||
</Menu>
|
||||
<ReactionPortals reactions={reactions} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
+18
-3
@@ -15,6 +15,7 @@ import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
|
||||
import { useTrackToggle } from '@livekit/components-react'
|
||||
import { Loader } from '@/primitives/Loader'
|
||||
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
|
||||
import { RiProhibited2Line } from '@remixicon/react'
|
||||
|
||||
enum BlurRadius {
|
||||
NONE = 0,
|
||||
@@ -63,6 +64,11 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
}, [videoTrack, videoTrack?.isMuted])
|
||||
|
||||
const clearEffect = async () => {
|
||||
await videoTrack.stopProcessor()
|
||||
onSubmit?.(undefined)
|
||||
}
|
||||
|
||||
const toggleEffect = async (
|
||||
type: ProcessorType,
|
||||
options: BackgroundOptions
|
||||
@@ -72,7 +78,7 @@ export const EffectsConfiguration = ({
|
||||
/**
|
||||
* Special case: if no video track is available, then we must pass directly the processor into the
|
||||
* toggle call. Otherwise, the rest of the function below would not have a videoTrack to call
|
||||
* setProccesor on.
|
||||
* setProcessor on.
|
||||
*
|
||||
* We arrive in this condition when we enter the room with the camera already off.
|
||||
*/
|
||||
@@ -95,8 +101,7 @@ export const EffectsConfiguration = ({
|
||||
try {
|
||||
if (isSelected(type, options)) {
|
||||
// Stop processor.
|
||||
await videoTrack.stopProcessor()
|
||||
onSubmit?.(undefined)
|
||||
await clearEffect()
|
||||
} else if (!processor || processor.serialize().type !== type) {
|
||||
// Change processor.
|
||||
const newProcessor = BackgroundProcessorFactory.getProcessor(
|
||||
@@ -243,6 +248,16 @@ export const EffectsConfiguration = ({
|
||||
gap: '1.25rem',
|
||||
})}
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
onPress={async () => {
|
||||
await clearEffect()
|
||||
}}
|
||||
isSelected={!getProcessor()}
|
||||
isDisabled={processorPendingReveal}
|
||||
>
|
||||
<RiProhibited2Line />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={tooltipLabel(ProcessorType.BLUR, {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css } from '@/styled-system/css'
|
||||
import { LeaveButton } from '../../components/controls/LeaveButton'
|
||||
import { SelectToggleDevice } from '../../components/controls/SelectToggleDevice'
|
||||
import { Track } from 'livekit-client'
|
||||
import { ReactionsToggle } from '../../components/controls/ReactionsToggle'
|
||||
import { HandToggle } from '../../components/controls/HandToggle'
|
||||
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
|
||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
||||
@@ -75,6 +76,7 @@ export function DesktopControlBar({
|
||||
}
|
||||
menuVariant="dark"
|
||||
/>
|
||||
<ReactionsToggle />
|
||||
{browserSupportsScreenSharing && (
|
||||
<ScreenShareToggle
|
||||
onDeviceError={(error) =>
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
"lowerHand": {
|
||||
"auto": "",
|
||||
"dismiss": ""
|
||||
},
|
||||
"reaction": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,12 @@
|
||||
"closed": ""
|
||||
},
|
||||
"support": "",
|
||||
"moreOptions": ""
|
||||
"moreOptions": "",
|
||||
"reactions": {
|
||||
"button": "",
|
||||
"send": "",
|
||||
"you": ""
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "",
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
"lowerHand": {
|
||||
"auto": "It seems you have started speaking, so your hand will be lowered.",
|
||||
"dismiss": "Keep hand raised"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reacted with {{emoji}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,12 @@
|
||||
"closed": "Show AI assistant"
|
||||
},
|
||||
"support": "Support",
|
||||
"moreOptions": "More options"
|
||||
"moreOptions": "More options",
|
||||
"reactions": {
|
||||
"button": "Send reaction",
|
||||
"send": "Send reaction {{emoji}}",
|
||||
"you": "you"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "More Options",
|
||||
@@ -200,7 +205,7 @@
|
||||
"fullScreen": "Full screen"
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "To avoid infinite loop display, do not share your entire screen or browser window. Instead, share a tab or another window.",
|
||||
"message": "To avoid infinite loop display, do not share your entire screen. Instead, share a tab or another window.",
|
||||
"stop": "Stop presenting",
|
||||
"ignore": "Ignore"
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
"lowerHand": {
|
||||
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
|
||||
"dismiss": "Laisser la main levée"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} a reagi avec {{emoji}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,12 @@
|
||||
"closed": "Afficher l'assistant IA"
|
||||
},
|
||||
"support": "Support",
|
||||
"moreOptions": "Plus d'options"
|
||||
"moreOptions": "Plus d'options",
|
||||
"reactions": {
|
||||
"button": "Envoyer une réaction",
|
||||
"send": "Envoyer la réaction {{emoji}}",
|
||||
"you": "vous"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "Plus d'options",
|
||||
@@ -200,7 +205,7 @@
|
||||
"fullScreen": "Plein écran"
|
||||
},
|
||||
"fullScreenWarning": {
|
||||
"message": "Pour éviter l'affichage en boucle infinie, ne partagez pas l'intégralité de votre écran ni de votre fenêtre de navigateur. Partagez plutôt un onglet ou une autre fenêtre.",
|
||||
"message": "Pour éviter l'affichage en boucle infinie, ne partagez pas l'intégralité de votre écran. Partagez plutôt un onglet ou une autre fenêtre.",
|
||||
"stop": "Arrêter la présentation",
|
||||
"ignore": "Ignorer"
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@ import { Box } from './Box'
|
||||
export const Menu = ({
|
||||
children,
|
||||
variant = 'light',
|
||||
placement,
|
||||
}: {
|
||||
children: [trigger: ReactNode, menu: ReactNode]
|
||||
variant?: 'dark' | 'light'
|
||||
placement?: 'bottom' | 'top' | 'left' | 'right'
|
||||
}) => {
|
||||
const [trigger, menu] = children
|
||||
return (
|
||||
<MenuTrigger>
|
||||
{trigger}
|
||||
<StyledPopover>
|
||||
<StyledPopover placement={placement}>
|
||||
<Box size="sm" type="popover" variant={variant}>
|
||||
{menu}
|
||||
</Box>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.14",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.14",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.0"
|
||||
version = "0.1.14"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Reference in New Issue
Block a user