Compare commits

...

10 Commits

Author SHA1 Message Date
lebaudantoine 2e8407ac7c 🚸(frontend) visually differentiate user's reactions from others
Implement light color variant for reactions triggered by current user versus
standard color for other participants' reactions. Provides visual cue to help
users easily identify their own emoji reactions in the conversation flow.
2025-05-05 23:27:22 +02:00
lebaudantoine 2af9ec0d85 ♻️(frontend) replace UTF emoji characters with designer-created images
Replace UTF character-based emoji with custom image assets designed by our
UX/UI team. Enhances cross-platform compatibility of reactions that were
previously inconsistent between operating systems. Specifically addresses
issue where emoji sent from Mac weren't properly displayed on all client
systems.
2025-05-05 23:27:22 +02:00
lebaudantoine b70799c2db 🔖(minor) bump release to 0.1.22
Upgrade track processor to benefits from webgl.
2025-05-05 23:26:49 +02:00
lebaudantoine 0facfc11be 🐛(frontend) fix video distortion when stopping processors
Prevent layout shift in vertical menu by adapting video element height based on
orientation. Eliminates glitchy effect where stopping a processor doubled
video height and pushed menu options downward for a few ms.
2025-05-03 19:13:29 +02:00
lebaudantoine 8023e44f71 🥚(frontend) add Konami code detector to unlock April Fool's effects
Implement easter egg that reveals fun features originally created for
April Fool's Day when users enter the Konami sequence.
2025-05-03 19:13:29 +02:00
lebaudantoine b27c5e9b92 🏗️(frontend) decouple landmark processor from background processors
Separate landmark processor logic to avoid entanglement with background
processing. Ensures future refactoring can replace custom background
implementation without affecting landmark functionality.
2025-05-03 19:13:29 +02:00
lebaudantoine 3e5c4c32e9 🔥(frontend) remove version upgrade warning for LiveKit WebGL update
Delete obsolete user notification now that the stable WebGL-powered version
with improved performance has been officially released.
2025-05-03 19:13:29 +02:00
lebaudantoine 551207ab86 🩹(frontend) add conditional stopProcessor call for cross-browser compat
Implement browser detection to explicitly stop processors in Firefox and other
browsers that lack full support for modern web APIs, before switching from
one processor to another.

This issue was introduced by recent upgrade of track processor.
An issue has been opened.
2025-05-03 19:13:29 +02:00
lebaudantoine 0aa47fcd1e ⬆️ (frontend) upgrade track processor to enable WebGL support
Update dependency to gain improved browser compatibility and WebGL
acceleration for better video processing performance.
2025-05-03 19:13:29 +02:00
virgile-deville c289f79d8e 📝(readme) add matrix room to readme
So that community members can gather and chat about meet

Signed-off-by: virgile-deville <virgile.deville@beta.gouv.fr>
2025-05-03 19:05:05 +02:00
32 changed files with 323 additions and 230 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
</p>
<p align="center">
<a href="https://livekit.io/">LiveKit</a> - <a href="https://go.crisp.chat/chat/embed/?website_id=58ea6697-8eba-4492-bc59-ad6562585041">Chat with us</a> - <a href="https://github.com/orgs/suitenumerique/projects/3/views/2">Roadmap</a> - <a href="https://github.com/suitenumerique/meet/blob/main/CHANGELOG.md">Changelog</a> - <a href="https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md">Bug reports</a>
<a href="https://livekit.io/">LiveKit</a> - <a href="https://matrix.to/#/#meet-official:matrix.org">Chat with us</a> - <a href="https://github.com/orgs/suitenumerique/projects/3/views/2">Roadmap</a> - <a href="https://github.com/suitenumerique/meet/blob/main/CHANGELOG.md">Changelog</a> - <a href="https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md">Bug reports</a>
</p>
<p align="center">
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.21"
version = "0.1.22"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+11 -16
View File
@@ -1,16 +1,16 @@
{
"name": "meet",
"version": "0.1.21",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.21",
"version": "0.1.22",
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.4",
"@livekit/track-processors": "0.3.3",
"@livekit/track-processors": "0.5.6",
"@pandacss/preset-panda": "0.53.3",
"@react-aria/toast": "3.0.1",
"@remixicon/react": "4.6.0",
@@ -1278,27 +1278,22 @@
}
},
"node_modules/@livekit/track-processors": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.3.3.tgz",
"integrity": "sha512-C5mBWe34ie0pfhCf9zAD65D26ZytzW/xGrj8Zc1BGh5kLISFDVQlefO78LZWw9K5saTamk60O4fzSCKqWMPMLA==",
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.6.tgz",
"integrity": "sha512-TlzObrSlp2PKor4VXqg6iefLRFVEb2T1lXwddBFdkPod60XVgYoMOj7V5xJm+UTE2MEtlE0003vUli9PyQGB1g==",
"license": "Apache-2.0",
"dependencies": {
"@mediapipe/holistic": "0.5.1675471629",
"@mediapipe/tasks-vision": "0.10.9"
"@mediapipe/tasks-vision": "0.10.14"
},
"peerDependencies": {
"livekit-client": "^1.12.0 || ^2.1.0"
}
},
"node_modules/@mediapipe/holistic": {
"version": "0.5.1675471629",
"resolved": "https://registry.npmjs.org/@mediapipe/holistic/-/holistic-0.5.1675471629.tgz",
"integrity": "sha512-qY+cxtDeSOvVtevrLgnodiwXYaAtPi7dHZtNv/bUCGEjFicAOYtMmrZSqMmbPkTB2+4jLnPF1vgshkAqQRSYAw=="
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.9.tgz",
"integrity": "sha512-/gFguyJm1ng4Qr7VVH2vKO+zZcQd8wc3YafUfvBuYFX0Y5+CvrV+VNPEVkl5W/gUZF5KNKNZAiaHPULGPCIjyQ=="
"version": "0.10.14",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.14.tgz",
"integrity": "sha512-vOifgZhkndgybdvoRITzRkIueWWSiCKuEUXXK6Q4FaJsFvRJuwgg++vqFUMlL0Uox62U5aEXFhHxlhV7Ja5e3Q==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.21",
"version": "0.1.22",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -15,7 +15,7 @@
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.4",
"@livekit/track-processors": "0.3.3",
"@livekit/track-processors": "0.5.6",
"@pandacss/preset-panda": "0.53.3",
"@react-aria/toast": "3.0.1",
"@remixicon/react": "4.6.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -11,7 +11,7 @@ import { useNotificationSound } from '@/features/notifications/hooks/useSoundNot
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { WaitingParticipantNotification } from './components/WaitingParticipantNotification'
import {
EMOJIS,
Emoji,
Reaction,
} from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import {
@@ -50,7 +50,7 @@ export const MainNotificationToast = () => {
}, [room, triggerNotificationSound])
const handleEmoji = (emoji: string, participant: Participant) => {
if (!emoji || !EMOJIS.includes(emoji)) return
if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return
const id = instanceIdRef.current++
setReactions((prev) => [
...prev,
@@ -15,6 +15,7 @@ export const INITIAL_POSITION = 200
interface FloatingReactionProps {
emoji: string
name?: string
isLocal?: boolean
speed?: number
scale?: number
}
@@ -22,6 +23,7 @@ interface FloatingReactionProps {
export function FloatingReaction({
emoji,
name,
isLocal = false,
speed = 1,
scale = 1,
}: FloatingReactionProps) {
@@ -60,7 +62,6 @@ export function FloatingReaction({
return (
<div
className={css({
fontSize: '3rem',
position: 'absolute',
display: 'flex',
alignItems: 'center',
@@ -69,23 +70,27 @@ export function FloatingReaction({
style={{
left: left,
bottom: INITIAL_POSITION + deltaY,
transform: `scale(${scale})`,
opacity: opacity,
}}
>
<span
<img
src={`/assets/reactions/${emoji}.png`}
alt={''}
className={css({
lineHeight: '57px',
height: '50px',
})}
>
{emoji}
</span>
style={{
transform: `scale(${scale})`,
transformOrigin: 'center bottom',
}}
/>
{name && (
<Text
variant="sm"
className={css({
backgroundColor: 'primaryDark.100',
color: 'white',
backgroundColor: isLocal ? 'primary.100' : 'primaryDark.100',
color: isLocal ? 'black' : 'white',
fontWeight: 500,
textAlign: 'center',
borderRadius: '20px',
paddingX: '0.5rem',
@@ -128,6 +133,7 @@ export function ReactionPortal({
speed={speed}
scale={scale}
name={participant?.isLocal ? t('you') : participant.name}
isLocal={participant?.isLocal}
/>
</div>,
document.body
@@ -1,4 +1,4 @@
import { ProcessorOptions, Track } from 'livekit-client'
import { ProcessorOptions, Track, TrackProcessor } from 'livekit-client'
import posthog from 'posthog-js'
import {
FilesetResolver,
@@ -11,19 +11,20 @@ import {
TIMEOUT_TICK,
timerWorkerScript,
} from './TimerWorker'
import {
BackgroundProcessorInterface,
BackgroundOptions,
ProcessorType,
} from '.'
import { ProcessorType } from '.'
const PROCESSING_WIDTH = 256 * 3
const PROCESSING_HEIGHT = 144 * 3
const FACE_LANDMARKS_CANVAS_ID = 'face-landmarks-local'
export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
options: BackgroundOptions
export type FaceLandmarksOptions = {
showGlasses: boolean
showFrench: boolean
}
export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
options: FaceLandmarksOptions
name: string
processedTrack?: MediaStreamTrack | undefined
@@ -50,7 +51,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
glassesImage?: HTMLImageElement
mustacheImage?: HTMLImageElement
beretImage?: HTMLImageElement
constructor(opts: BackgroundOptions) {
constructor(opts: FaceLandmarksOptions) {
this.name = 'face_landmarks'
this.options = opts
this.type = ProcessorType.FACE_LANDMARKS
@@ -314,7 +315,7 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
return element
}
update(opts: BackgroundOptions): void {
update(opts: FaceLandmarksOptions): void {
this.options = opts
}
@@ -3,13 +3,10 @@ import { Track, TrackProcessor } from 'livekit-client'
import { BackgroundBlurTrackProcessorJsWrapper } from './BackgroundBlurTrackProcessorJsWrapper'
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
import { BackgroundVirtualTrackProcessorJsWrapper } from './BackgroundVirtualTrackProcessorJsWrapper'
import { FaceLandmarksProcessor } from './FaceLandmarksProcessor'
export type BackgroundOptions = {
blurRadius?: number
imagePath?: string
showGlasses?: boolean
showFrench?: boolean
}
export interface ProcessorSerialized {
@@ -32,12 +29,12 @@ export enum ProcessorType {
}
export class BackgroundProcessorFactory {
static hasModernApiSupport() {
return ProcessorWrapper.hasModernApiSupport
}
static isSupported() {
return (
ProcessorWrapper.isSupported ||
BackgroundCustomProcessor.isSupported ||
FaceLandmarksProcessor.isSupported
)
return ProcessorWrapper.isSupported || BackgroundCustomProcessor.isSupported
}
static getProcessor(
@@ -58,10 +55,6 @@ export class BackgroundProcessorFactory {
if (BackgroundCustomProcessor.isSupported) {
return new BackgroundCustomProcessor(opts)
}
} else if (type === ProcessorType.FACE_LANDMARKS) {
if (FaceLandmarksProcessor.isSupported) {
return new FaceLandmarksProcessor(opts)
}
}
return undefined
}
@@ -15,7 +15,15 @@ import { Participant } from 'livekit-client'
import useRateLimiter from '@/hooks/useRateLimiter'
// eslint-disable-next-line react-refresh/only-export-components
export const EMOJIS = ['👍', '👎', '👏', '❤️', '😂', '😮', '🎉']
export enum Emoji {
THUMBS_UP = 'thumbs-up',
THUMBS_DOWN = 'thumbs-down',
CLAP = 'clapping-hands',
HEART = 'red-heart',
LAUGHING = 'face-with-tears-of-joy',
SURPRISED = 'face-with-open-mouth',
CELEBRATION = 'party-popper',
}
export interface Reaction {
id: number
@@ -112,10 +120,10 @@ export const ReactionsToggle = () => {
<div
className={css({
position: 'absolute',
top: -55,
left: -114,
top: -63,
left: -139,
borderRadius: '8px',
padding: '0.25rem',
padding: '0.35rem',
backgroundColor: 'primaryDark.50',
opacity: opacity,
transition: 'opacity 0.2s ease',
@@ -129,24 +137,29 @@ export const ReactionsToggle = () => {
<RACToolbar
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
{EMOJIS.map((emoji, index) => (
{Object.values(Emoji).map((emoji, index) => (
<Button
key={index}
onPress={() => debouncedSendReaction(emoji)}
aria-label={t('send', { emoji })}
variant="primaryTextDark"
size="sm"
square
data-attr={`send-reaction-${emoji}`}
>
<span
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
fontSize: '20px',
minHeight: '28px',
minWidth: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
>
{emoji}
</span>
/>
</Button>
))}
</RACToolbar>
@@ -15,12 +15,9 @@ import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import {
RiProhibited2Line,
RiGlassesLine,
RiGoblet2Fill,
} from '@remixicon/react'
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
import { RiProhibited2Line } from '@remixicon/react'
import { FunnyEffects } from './FunnyEffects'
import { useHasFunnyEffectsAccess } from '../../hooks/useHasFunnyEffectsAccess'
enum BlurRadius {
NONE = 0,
@@ -55,7 +52,7 @@ export const EffectsConfiguration = ({
const { toggle, enabled } = useTrackToggle({ source: Track.Source.Camera })
const [processorPending, setProcessorPending] = useState(false)
const processorPendingReveal = useSyncAfterDelay(processorPending)
const hasFaceLandmarksAccess = useHasFaceLandmarksAccess()
const hasFunnyEffectsAccess = useHasFunnyEffectsAccess()
useEffect(() => {
const videoElement = videoRef.current
@@ -114,6 +111,13 @@ export const EffectsConfiguration = ({
type,
options
)!
// IMPORTANT: Must explicitly stop previous processor before setting a new one
// in browsers without modern API support to prevent UI crashes.
// This workaround is needed until this issue is resolved:
// https://github.com/livekit/track-processors-js/issues/85
if (!BackgroundProcessorFactory.hasModernApiSupport()) {
await videoTrack.stopProcessor()
}
await videoTrack.setProcessor(newProcessor)
onSubmit?.(newProcessor)
} else {
@@ -145,42 +149,9 @@ export const EffectsConfiguration = ({
}
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
if (type === ProcessorType.FACE_LANDMARKS) {
const effect = options.showGlasses ? 'glasses' : 'french'
return t(
`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`
)
}
return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
}
const getFaceLandmarksOptions = () => {
const processor = getProcessor()
if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) {
return processor.serialize().options as {
showGlasses?: boolean
showFrench?: boolean
}
}
return { showGlasses: false, showFrench: false }
}
const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'french') => {
const currentOptions = getFaceLandmarksOptions()
const newOptions = {
...currentOptions,
[effect === 'glasses' ? 'showGlasses' : 'showFrench']:
!currentOptions[effect === 'glasses' ? 'showGlasses' : 'showFrench'],
}
if (!newOptions.showGlasses && !newOptions.showFrench) {
// If both effects are off stop the processor
await clearEffect()
} else {
await toggleEffect(ProcessorType.FACE_LANDMARKS, newOptions)
}
}
return (
<div
className={css(
@@ -215,7 +186,7 @@ export const EffectsConfiguration = ({
muted
style={{
transform: 'rotateY(180deg)',
minHeight: '175px',
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
borderRadius: '8px',
}}
/>
@@ -268,6 +239,13 @@ export const EffectsConfiguration = ({
: {}
)}
>
{hasFunnyEffectsAccess && (
<FunnyEffects
videoTrack={videoTrack}
isPending={processorPendingReveal}
onPending={setProcessorPending}
/>
)}
{isSupported ? (
<>
<div>
@@ -340,8 +318,6 @@ export const EffectsConfiguration = ({
<BlurOnStrong />
</ToggleButton>
</div>
</div>
{hasFaceLandmarksAccess && (
<div
className={css({
marginTop: '1.5rem',
@@ -354,114 +330,51 @@ export const EffectsConfiguration = ({
}}
variant="bodyXsBold"
>
{t('faceLandmarks.title')}
{t('virtual.title')}
</H>
<div
className={css({
display: 'flex',
gap: '1.25rem',
flexWrap: 'wrap',
})}
>
<ToggleButton
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showFrench: false,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: true,
showFrench: false,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleFaceLandmarkEffect('glasses')
}
isSelected={getFaceLandmarksOptions().showGlasses}
data-attr="toggle-glasses"
>
<RiGlassesLine />
</ToggleButton>
<ToggleButton
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showFrench: true,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showFrench: true,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleFaceLandmarkEffect('french')
}
isSelected={getFaceLandmarksOptions().showFrench}
data-attr="toggle-french"
>
<RiGoblet2Fill />
</ToggleButton>
</div>
</div>
)}
<div
className={css({
marginTop: '1.5rem',
})}
>
<H
lvl={3}
style={{
marginBottom: '1rem',
}}
variant="bodyXsBold"
>
{t('virtual.title')}
</H>
<div
className={css({
display: 'flex',
gap: '1.25rem',
flexWrap: 'wrap',
})}
>
{[...Array(8).keys()].map((i) => {
const imagePath = `/assets/backgrounds/${i + 1}.jpg`
const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg`
return (
<ToggleButton
key={i}
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.VIRTUAL, {
imagePath,
})}
tooltip={tooltipLabel(ProcessorType.VIRTUAL, {
imagePath,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleEffect(ProcessorType.VIRTUAL, {
{[...Array(8).keys()].map((i) => {
const imagePath = `/assets/backgrounds/${i + 1}.jpg`
const thumbnailPath = `/assets/backgrounds/thumbnails/${i + 1}.jpg`
return (
<ToggleButton
key={i}
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.VIRTUAL, {
imagePath,
})
}
isSelected={isSelected(ProcessorType.VIRTUAL, {
imagePath,
})}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${thumbnailPath})`,
}}
data-attr={`toggle-virtual-${i}`}
/>
)
})}
})}
tooltip={tooltipLabel(ProcessorType.VIRTUAL, {
imagePath,
})}
isDisabled={processorPendingReveal}
onChange={async () =>
await toggleEffect(ProcessorType.VIRTUAL, {
imagePath,
})
}
isSelected={isSelected(ProcessorType.VIRTUAL, {
imagePath,
})}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${thumbnailPath})`,
}}
data-attr={`toggle-virtual-${i}`}
/>
)
})}
</div>
</div>
</div>
</div>
<Information className={css({ marginTop: '1rem' })}>
<Text variant="sm"> {t('experimental')}</Text>
</Information>
</>
) : (
<Information>
@@ -0,0 +1,120 @@
import { css } from '@/styled-system/css'
import { H, ToggleButton } from '@/primitives'
import { ProcessorType } from '../blur'
import { RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor'
import { LocalVideoTrack } from 'livekit-client'
export type FunnyEffectsProps = {
videoTrack: LocalVideoTrack
isPending?: boolean
onPending: (value: boolean) => void
}
export const FunnyEffects = ({
videoTrack,
isPending,
onPending,
}: FunnyEffectsProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'effects' })
const getOptions = () => {
const processor = videoTrack?.getProcessor() as FaceLandmarksProcessor
if (!processor || processor.type != ProcessorType.FACE_LANDMARKS) {
return {
showGlasses: false,
showFrench: false,
}
}
return processor.serialize().options
}
const options = getOptions()
const toggleFaceLandmarkEffect = async (
showEffect: 'showGlasses' | 'showFrench'
) => {
const options = getOptions()
const processor = videoTrack?.getProcessor() as FaceLandmarksProcessor
const newOptions = {
...options,
[showEffect]: !options[showEffect],
}
onPending(true)
try {
if (!newOptions.showGlasses && !newOptions.showFrench) {
await videoTrack.stopProcessor()
} else if (options.showGlasses || options.showFrench) {
await processor?.update(newOptions)
} else {
const newProcessor = new FaceLandmarksProcessor(newOptions)
await videoTrack.setProcessor(newProcessor)
}
} catch (e) {
console.error('could not update processor', e)
} finally {
onPending(false)
}
}
const getLabelAction = (enabled: boolean) => (enabled ? 'clear' : 'apply')
return (
<div
className={css({
marginBottom: '1.5rem',
})}
>
<H
lvl={3}
style={{
marginBottom: '1rem',
}}
variant="bodyXsBold"
>
{t('faceLandmarks.title')}
</H>
<div
className={css({
display: 'flex',
gap: '1.25rem',
})}
>
<ToggleButton
variant="bigSquare"
aria-label={t(
`faceLandmarks.glasses.${getLabelAction(options.showGlasses)}`
)}
tooltip={t(
`faceLandmarks.glasses.${getLabelAction(options.showGlasses)}`
)}
isDisabled={isPending}
onChange={async () => await toggleFaceLandmarkEffect('showGlasses')}
isSelected={options.showGlasses}
data-attr="toggle-glasses"
>
<RiGlassesLine />
</ToggleButton>
<ToggleButton
variant="bigSquare"
aria-label={t(
`faceLandmarks.french.${getLabelAction(options.showFrench)}`
)}
tooltip={t(
`faceLandmarks.french.${getLabelAction(options.showFrench)}`
)}
isDisabled={isPending}
onChange={async () => await toggleFaceLandmarkEffect('showFrench')}
isSelected={options.showFrench}
data-attr="toggle-french"
>
<RiGoblet2Fill />
</ToggleButton>
</div>
</div>
)
}
@@ -1,10 +0,0 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { FeatureFlags } from '@/features/analytics/enums'
export const useHasFaceLandmarksAccess = () => {
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
return featureEnabled || !isAnalyticsEnabled
}
@@ -0,0 +1,22 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { FeatureFlags } from '@/features/analytics/enums'
import useKonami from '@/features/rooms/livekit/hooks/useKonami'
import { konamiStore } from '@/stores/konami'
import { useSnapshot } from 'valtio'
export const useHasFunnyEffectsAccess = () => {
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const konamiSnap = useSnapshot(konamiStore)
useKonami(
() =>
(konamiStore.areFunnyEffectsEnabled = !konamiSnap.areFunnyEffectsEnabled)
)
return (
(featureEnabled || !isAnalyticsEnabled) && konamiSnap.areFunnyEffectsEnabled
)
}
@@ -0,0 +1,35 @@
/**
* Konami Code Detector Component
* This implementation is taken from: vmarchesin/react-konami-code
*/
import { useCallback, useEffect, useState } from 'react'
export const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]
function useKonami(action: () => void, { code = KONAMI_CODE } = {}) {
const [input, setInput] = useState<number[]>([])
const onKeyUp = useCallback(
(e: KeyboardEvent) => {
const newInput = input
newInput.push(e.keyCode)
newInput.splice(-code.length - 1, input.length - code.length)
setInput(newInput)
if (newInput.join('').includes(code.join(''))) {
action()
}
},
[input, setInput, code, action]
)
useEffect(() => {
document.addEventListener('keyup', onKeyUp)
return () => {
document.removeEventListener('keyup', onKeyUp)
}
}, [onKeyUp])
}
export default useKonami
+1 -2
View File
@@ -164,8 +164,7 @@
"apply": "Französische Touch hinzufügen",
"clear": "Französische Touch entfernen"
}
},
"experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität."
}
},
"sidePanel": {
"heading": {
+1 -2
View File
@@ -163,8 +163,7 @@
"apply": "Add French touch",
"clear": "Remove French touch"
}
},
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
}
},
"sidePanel": {
"heading": {
+1 -2
View File
@@ -163,8 +163,7 @@
"apply": "Ajouter la touche française",
"clear": "Retirer la touche française"
}
},
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité."
}
},
"sidePanel": {
"heading": {
+1 -2
View File
@@ -163,8 +163,7 @@
"apply": "Franse stijl toevoegen",
"clear": "Franse stijl verwijderen"
}
},
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."
}
},
"sidePanel": {
"heading": {
+9
View File
@@ -0,0 +1,9 @@
import { proxy } from 'valtio'
type State = {
areFunnyEffectsEnabled: boolean
}
export const konamiStore = proxy<State>({
areFunnyEffectsEnabled: false,
})
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.21",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.21",
"version": "0.1.22",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.21",
"version": "0.1.22",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+1 -1
View File
@@ -4,7 +4,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.1.21",
"version": "0.1.22",
"type": "module",
"main": "./dist/visio-sdk.umd.cjs",
"module": "./dist/visio-sdk.js",
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.21",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.21",
"version": "0.1.22",
"license": "ISC",
"workspaces": [
"./library",
@@ -620,7 +620,7 @@
},
"library": {
"name": "@gouvfr-lasuite/visio-sdk",
"version": "0.1.21",
"version": "0.1.22",
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.21",
"version": "0.1.22",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.21"
version = "0.1.22"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",