3acd84bf76
Audit-driven fixes for the audio-room reactions overlay: 1. RoomReactionsAggregator now dedups by event id. The previous code appended every event to a flat list, but LocalCache.observeEvents re-emits the full matching list on every cache mutation — so an N-reaction window grew quadratically and the overlay rendered the same emoji once per replay. Keyed by event id collapses re-emits. 2. RoomPresenceAggregator gains applyOrNull that returns null when an incoming heartbeat is older-or-equal to the cached presence; the VM skips the StateFlow write in that case. In a 200-peer room, every replay used to copy a 200-entry map plus run an O(N) StateFlow equality check 200 times per emission. Now it's one-and-skip. 3. ReactionsEvictionTicker only ticks while the aggregator is non-empty. Once the last reaction expires the loop self-cancels until the next reaction lands — a quiet room costs no scheduled work. 4. REACTION_WINDOW_SEC dropped 30s -> 10s. Reactions are about what the speaker is saying right now; a 10s window keeps the overlay timely instead of bleeding into the next paragraph. 5. SpeakerReactionOverlay drives a per-chip lifecycle animation: each chip drifts upward ~16dp and fades over the 10s window. A fresh reaction (same emoji) restarts the chip's animation. The AnimatedVisibility outer entry/exit still smooths first-arrival and final disappearance. Tests: added a re-emit dedup case to RoomReactionsStateTest plus an end-to-end replay assertion in NestViewModelTest. Existing tests updated to use unique event ids. https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4