From e9e78c22b5dc6f0e7a4887b828a4d95c7e03c929 Mon Sep 17 00:00:00 2001 From: fynnomenon <82415981+fynnomenon@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:58:10 +0000 Subject: [PATCH 1/2] feat: add interruption animation --- joinly/providers/browser/camera_feed.py | 23 ++++++++++++++++++++ joinly/providers/browser/meeting_provider.py | 11 ++++++++++ joinly/session.py | 7 +++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/joinly/providers/browser/camera_feed.py b/joinly/providers/browser/camera_feed.py index 858448b..e1530c4 100644 --- a/joinly/providers/browser/camera_feed.py +++ b/joinly/providers/browser/camera_feed.py @@ -140,6 +140,26 @@ } }""" +# Interrupted: dots scatter outward from center and fade +_FX_INTERRUPTED = """\ +function fxInterrupted(ctx, cx, y, t, alpha) { + const N = 5, r = H * 0.007; + for (let i = 0; i < N; i++) { + const angle = (i / N) * Math.PI * 2 + t * 1.5; + const p = (t * 2.5 + i / N) % 1; + const spread = H * 0.01 + p * H * 0.04; + const dx = Math.cos(angle) * spread; + const dy = Math.sin(angle) * spread * 0.5; + const fade = (1 - p) * alpha; + if (fade < 0.01) continue; + ctx.globalAlpha = fade; + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(cx + dx, y + dy, r * (1 - p * 0.5), 0, Math.PI * 2); + ctx.fill(); + } +}""" + # Reading: dot sweeping back and forth with a trail _FX_READING = """\ function fxReading(ctx, cx, y, t, alpha) { @@ -175,11 +195,13 @@ {fx_typing} {fx_share} {fx_reading} + {fx_interrupted} const FX = {{ send_chat_message: fxTyping, get_chat_history: fxReading, get_participants: fxReading, + interrupted: fxInterrupted, }}; function _initCanvas() {{ @@ -366,6 +388,7 @@ async def install(self, meeting_page: Page) -> None: fx_typing=_FX_TYPING, fx_share=_FX_SHARE, fx_reading=_FX_READING, + fx_interrupted=_FX_INTERRUPTED, ) await meeting_page.add_init_script(script) diff --git a/joinly/providers/browser/meeting_provider.py b/joinly/providers/browser/meeting_provider.py index 9d525b8..db8ef9d 100644 --- a/joinly/providers/browser/meeting_provider.py +++ b/joinly/providers/browser/meeting_provider.py @@ -30,6 +30,7 @@ MeetingChatHistory, MeetingParticipant, ProviderNotSupportedError, + SpeechInterruptedError, VideoSnapshot, ) @@ -207,6 +208,16 @@ async def _action_guard( finally: self._camera_feed.set_status("") + @asynccontextmanager + async def speech_guard(self) -> AsyncIterator[None]: + """Context manager that sets the interrupted camera status on interruption.""" + try: + yield + except SpeechInterruptedError: + self._camera_feed.set_status("interrupted") + self._camera_feed.set_status("") + raise + async def _get_platform_controller(self, url: str) -> BrowserPlatformController: """Get the platform-specific meeting controller based on the URL. diff --git a/joinly/session.py b/joinly/session.py index 3ece0ea..f20ce2e 100644 --- a/joinly/session.py +++ b/joinly/session.py @@ -128,7 +128,12 @@ async def speak_text(self, text: str) -> None: Args: text (str): The text to be spoken. """ - await self._speech_controller.speak_text(text) + guard = getattr(self._meeting_provider, "speech_guard", None) + if guard is not None: + async with guard(): + await self._speech_controller.speak_text(text) + else: + await self._speech_controller.speak_text(text) async def send_chat_message(self, message: str) -> None: """Send a chat message in the meeting. From 57265407d1c3580269d25c50b11395721a5d9cf4 Mon Sep 17 00:00:00 2001 From: fynnomenon <82415981+fynnomenon@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:44:28 +0000 Subject: [PATCH 2/2] fix: resolve missed rename in merge conflict (stupid) --- joinly/providers/browser/meeting_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/joinly/providers/browser/meeting_provider.py b/joinly/providers/browser/meeting_provider.py index c4eae3b..ad058ea 100644 --- a/joinly/providers/browser/meeting_provider.py +++ b/joinly/providers/browser/meeting_provider.py @@ -217,8 +217,8 @@ async def speech_guard(self) -> AsyncIterator[None]: try: yield except SpeechInterruptedError: - self._camera_feed.set_status("interrupted") - self._camera_feed.set_status("") + self._camera_feed.set_effect("interrupted") + self._camera_feed.set_effect(None) raise async def _get_platform_controller(self, url: str) -> BrowserPlatformController: