From c4335d2809314d6833a37d83c4e898847f718168 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 13 Aug 2026 10:39:01 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B(frontend)=20implement=20hyster?= =?UTF-8?q?esis=20band=20for=20the=20control=20bar=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce dual thresholds (1100px wide, 1050px narrow) for switching the control bar between the expanded inline controls and the collapsed menu. The 50px deadband absorbs the width changes caused by rendering 5 buttons vs. 1 button, preventing an infinite layout oscillation and the resulting `ResizeObserver loop` errors. --- CHANGELOG.md | 1 + .../prefabs/ControlBar/MoreOptions.tsx | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaac76e904..6acf269659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - 📈(frontend) downgrade unreachable external home URL from error to event - 🐛(frontend) handle 401 responses when syncing user preferences - 🐛(frontend) harden speaker test against missing sinks and play errors +- 🐛(frontend) implement hysteresis band for the control bar layout ## [1.26.0] - 2026-08-12 diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MoreOptions.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MoreOptions.tsx index 582d08c158..d7b5e064af 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MoreOptions.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar/MoreOptions.tsx @@ -12,7 +12,8 @@ import type { ToggleButtonProps } from '@/primitives/ToggleButton' import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' -const CONTROL_BAR_BREAKPOINT = 1100 +const CONTROL_BAR_BREAKPOINT_WIDE = 1100 +const CONTROL_BAR_BREAKPOINT_NARROW = 1050 const NavigationControls = ({ onPress, @@ -65,10 +66,9 @@ export const LateralMenu = () => { ) } - interface BreakpointObserverProps { containerRef: RefObject - onWideChange: (isWide: boolean) => void + onWideChange: (isWide: boolean | null) => void } const BreakpointObserver = ({ @@ -76,7 +76,20 @@ const BreakpointObserver = ({ onWideChange, }: BreakpointObserverProps) => { const { width } = useSize(containerRef) - const isWide = width > CONTROL_BAR_BREAKPOINT + const [isWide, setIsWide] = useState(null) + + useEffect(() => { + if (!width) { + return + } + if (width > CONTROL_BAR_BREAKPOINT_WIDE) { + setIsWide(true) + } else if (width <= CONTROL_BAR_BREAKPOINT_NARROW) { + setIsWide(false) + } else { + setIsWide((prev) => (prev === null ? false : prev)) + } + }, [width]) useEffect(() => { onWideChange(isWide) @@ -90,7 +103,7 @@ export const MoreOptions = ({ }: { parentElement: RefObject }) => { - const [isWide, setIsWide] = useState(false) + const [isWide, setIsWide] = useState(null) return ( ) } From 4c0d89ef812755db088726b466ce0ce19099ea5a Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 13 Aug 2026 11:15:40 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B(frontend)=20vendor=20formatCha?= =?UTF-8?q?tMessageLinks=20and=20trim=20surrounding=20newlines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy the `formatChatMessageLinks` function locally so we can iterate on it without patching the upstream dependency. Use the local copy to trim `\n` characters at the beginning and end of chat messages, which were leaking into the rendered output. --- CHANGELOG.md | 1 + .../chat/components/ChatMessageBody.tsx | 2 +- src/frontend/src/features/chat/utils.tsx | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/features/chat/utils.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6acf269659..44fce3fdff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Changed - 🔥(frontend) drop unused vendored ConnectionObserver +- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines ### Fixed diff --git a/src/frontend/src/features/chat/components/ChatMessageBody.tsx b/src/frontend/src/features/chat/components/ChatMessageBody.tsx index 67381ea4d6..f9decfdcaa 100644 --- a/src/frontend/src/features/chat/components/ChatMessageBody.tsx +++ b/src/frontend/src/features/chat/components/ChatMessageBody.tsx @@ -1,6 +1,6 @@ import { ChatRow } from '@/stores/chat' import React, { useMemo } from 'react' -import { formatChatMessageLinks } from '@livekit/components-react' +import { formatChatMessageLinks } from '../utils' import { css } from '@/styled-system/css' import { Text } from '@/primitives' diff --git a/src/frontend/src/features/chat/utils.tsx b/src/frontend/src/features/chat/utils.tsx new file mode 100644 index 0000000000..7542e3b1bf --- /dev/null +++ b/src/frontend/src/features/chat/utils.tsx @@ -0,0 +1,32 @@ +import { tokenize, createDefaultGrammar } from '@livekit/components-core' +import { ReactNode } from 'react' + +const defaultGrammar = Object.freeze(createDefaultGrammar()) + +export function formatChatMessageLinks(message: string): ReactNode { + const trimmedMessage = message.replace(/^[\r\n]+|[\r\n]+$/g, '') + return tokenize(trimmedMessage, defaultGrammar).map((tok, i) => { + if (typeof tok === `string`) { + return tok + } else { + const content = tok.content.toString() + const href = + tok.type === `url` + ? /^http(s?):\/\//.test(content) + ? content + : `https://${content}` + : `mailto:${content}` + return ( + + {content} + + ) + } + }) +} From 9392cd3e30b867dc636a1d4a5c264121935881a0 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 13 Aug 2026 11:21:39 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B(frontend)=20fix=20toolbar=20Re?= =?UTF-8?q?sizeObserver=20loop=20and=20alignment=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch toolbar horizontal alignment from `marginRight` to `transform: translateX()`, so it no longer triggers layout reflows during ResizeObserver cycles and stops the "ResizeObserver loop" error. * Replace the unstable `shift * 2` margin heuristic with a direct 1:1 positional delta (`offsetX + shift`). * Decouple CSS transitions: use the individual CSS `translate` property for the slide-up/down animations, leaving `transform` free for dynamic horizontal positioning. --- CHANGELOG.md | 1 + .../toolbar/ReactionButtonsContainer.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fce3fdff..36b6ce2b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - 🐛(frontend) handle 401 responses when syncing user preferences - 🐛(frontend) harden speaker test against missing sinks and play errors - 🐛(frontend) implement hysteresis band for the control bar layout +- 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift ## [1.26.0] - 2026-08-12 diff --git a/src/frontend/src/features/reactions/components/toolbar/ReactionButtonsContainer.tsx b/src/frontend/src/features/reactions/components/toolbar/ReactionButtonsContainer.tsx index 07e91a5f9c..acfb484b04 100644 --- a/src/frontend/src/features/reactions/components/toolbar/ReactionButtonsContainer.tsx +++ b/src/frontend/src/features/reactions/components/toolbar/ReactionButtonsContainer.tsx @@ -26,8 +26,8 @@ const StyledContainer = styled('div', { backgroundColor: 'primaryDark.100', maxWidth: '100%', opacity: 0, - transform: 'translateY(3.25rem)', - transition: 'opacity, transform', + translate: '0 3.25rem', + transition: 'opacity, translate', transitionDuration: '0.5s', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', pointerEvents: 'none', @@ -36,7 +36,7 @@ const StyledContainer = styled('div', { isVisible: { true: { opacity: 1, - transform: 'translateY(0)', + translate: '0 0', pointerEvents: 'auto', }, }, @@ -84,7 +84,7 @@ export const ReactionButtonsContainer = ({ shouldBeCenteredWithToggleButton, setShouldBeCenteredWithToggleButton, ] = useState(false) - const [rightOffset, setRightOffset] = useState(0) + const [offsetX, setOffsetX] = useState(0) const updateArrows = useCallback(() => { const el = scrollRef.current @@ -115,7 +115,7 @@ export const ReactionButtonsContainer = ({ useLayoutEffect(() => { if (!shouldBeCenteredWithToggleButton || isMobile) { - setRightOffset(0) + setOffsetX(0) return } @@ -133,7 +133,7 @@ export const ReactionButtonsContainer = ({ const containerCenterX = containerRect.left + containerRect.width / 2 const shift = toggleCenterX - containerCenterX if (Math.abs(shift) < 0.5) return - setRightOffset((prev) => prev - shift * 2) + setOffsetX((prev) => prev + shift) } const schedule = () => { @@ -182,7 +182,7 @@ export const ReactionButtonsContainer = ({ isVisible={isVisible} style={ shouldBeCenteredWithToggleButton && !isMobile && adjustedCentering - ? { marginRight: `${rightOffset}px` } + ? { transform: `translateX(${offsetX}px)` } : { margin: '0 15px' } } > From f75adef69fdee7eb1244515d206c32cc4a525c73 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 13 Aug 2026 11:56:13 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B(analytics)=20filter=20benign?= =?UTF-8?q?=20ResizeObserver=20loop=20error=20in=20Sentry/PostHog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filter out harmless `ResizeObserver loop limit exceeded` and `ResizeObserver loop completed with undelivered notifications` errors via `beforeSend`. Why this is safe: * These are W3C spec-mandated browser guards that defer notification delivery to the next frame when callbacks alter layout during render. They do not cause JS runtime exceptions or break the UX. Why we actually need to filter them: * Telemetry platforms like PostHog do not stack/group these well, frequently generating distinct error events per browser engine and version. * The unique variants flood reporting dashboards and trigger false-positive alerts that clutter real issue triage. --- CHANGELOG.md | 1 + .../features/analytics/exceptionFilters.ts | 24 +++++++++++++++++++ .../features/analytics/hooks/useAnalytics.ts | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 src/frontend/src/features/analytics/exceptionFilters.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b6ce2b3b..1078d41061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to - 🐛(frontend) harden speaker test against missing sinks and play errors - 🐛(frontend) implement hysteresis band for the control bar layout - 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift +- 🐛(analytics) filter benign ResizeObserver loop error in Sentry/PostHog ## [1.26.0] - 2026-08-12 diff --git a/src/frontend/src/features/analytics/exceptionFilters.ts b/src/frontend/src/features/analytics/exceptionFilters.ts new file mode 100644 index 0000000000..9a11cfe905 --- /dev/null +++ b/src/frontend/src/features/analytics/exceptionFilters.ts @@ -0,0 +1,24 @@ +import type { CaptureResult } from 'posthog-js' + +const IGNORED_EXCEPTION_PATTERNS = [ + /ResizeObserver loop (completed with undelivered notifications|limit exceeded)/, +] + +const shouldIgnoreException = (value: unknown): boolean => + typeof value === 'string' && + IGNORED_EXCEPTION_PATTERNS.some((pattern) => pattern.test(value)) + +export const filterExceptions = ( + event: CaptureResult | null +): CaptureResult | null => { + if (event?.event !== '$exception') return event + + const exceptionList = event.properties?.['$exception_list'] + const values: unknown[] = Array.isArray(exceptionList) + ? exceptionList.map((exception) => exception?.value) + : [] + + values.push(event.properties?.['$exception_message']) + + return values.some(shouldIgnoreException) ? null : event +} diff --git a/src/frontend/src/features/analytics/hooks/useAnalytics.ts b/src/frontend/src/features/analytics/hooks/useAnalytics.ts index a1c2d37241..1729bea28e 100644 --- a/src/frontend/src/features/analytics/hooks/useAnalytics.ts +++ b/src/frontend/src/features/analytics/hooks/useAnalytics.ts @@ -2,6 +2,7 @@ import { useEffect } from 'react' import { type ApiUser } from '@/features/auth/api/ApiUser' import { useUser } from '@/features/auth/api/useUser' import { getPosthog } from '../utils' +import { filterExceptions } from '../exceptionFilters' export const startAnalyticsSession = (data: ApiUser) => { getPosthog().then((ph) => { @@ -47,6 +48,7 @@ export const useAnalytics = ({ capture_unhandled_rejections: true, capture_console_errors: true, }, + before_send: filterExceptions, }) }) }, [id, host, flags_api_host, isDisabled])