From 99cd1cfa69aab41efe533bcaa42a2126a21d8f8f Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Tue, 5 May 2026 17:20:32 +0300 Subject: [PATCH 1/3] fix: standup feedback round - Tighten chat message spacing. - Float reaction tray on hover so empty messages no longer reserve a row; flip placement for the topmost message to avoid the side-panel tabs. - Always show all quick reactions in the floating tray and toggle when re-clicking an already-reacted emoji. - Make reactions toolbar opaque and drop per-emoji tooltips. - Click on a focused video tile now defocuses it. - Prefer the system default mic/camera (deviceId === 'default') over the first enumerated device. - Raise the default free-for-all speaker limit to 100. --- .../liveRooms/CreateLiveRoomForm.tsx | 2 +- .../liveRooms/LiveRoomChatPanel.tsx | 14 +- .../liveRooms/LiveRoomChatReactions.tsx | 145 ++++++++++-------- .../liveRooms/LiveRoomReactionsToolbar.tsx | 65 ++++---- .../liveRooms/LiveRoomVideoTile.tsx | 8 + .../shared/src/contexts/LiveRoomContext.tsx | 10 +- 6 files changed, 137 insertions(+), 107 deletions(-) diff --git a/packages/shared/src/components/liveRooms/CreateLiveRoomForm.tsx b/packages/shared/src/components/liveRooms/CreateLiveRoomForm.tsx index 124a9ea3129..82866a0bf4a 100644 --- a/packages/shared/src/components/liveRooms/CreateLiveRoomForm.tsx +++ b/packages/shared/src/components/liveRooms/CreateLiveRoomForm.tsx @@ -31,7 +31,7 @@ import Link from '../utilities/Link'; import { LogEvent } from '../../lib/log'; import styles from './CreateLiveRoomForm.module.css'; -const DEFAULT_FREE_FOR_ALL_SPEAKER_LIMIT = 4; +const DEFAULT_FREE_FOR_ALL_SPEAKER_LIMIT = 100; const DEFAULT_SCHEDULE_DELAY_MS = 30 * 60 * 1000; const DESCRIPTION_MAX_LENGTH = 4000; diff --git a/packages/shared/src/components/liveRooms/LiveRoomChatPanel.tsx b/packages/shared/src/components/liveRooms/LiveRoomChatPanel.tsx index f4e024ed837..4f6851a12e7 100644 --- a/packages/shared/src/components/liveRooms/LiveRoomChatPanel.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoomChatPanel.tsx @@ -243,7 +243,10 @@ export const LiveRoomChatPanel = ({ const [pickerMessageId, setPickerMessageId] = useState(null); const longPressHandlers = useTouchLongPress({ enabled: isMobile && canChat, - onLongPress: setPickerMessageId, + onLongPress: (messageId) => { + window.getSelection()?.removeAllRanges(); + setPickerMessageId(messageId); + }, }); const pickerMessage = pickerMessageId @@ -350,7 +353,7 @@ export const LiveRoomChatPanel = ({
{chatMessages.length === 0 ? (
@@ -370,7 +373,7 @@ export const LiveRoomChatPanel = ({
) : ( - chatMessages.map((message) => { + chatMessages.map((message, messageIndex) => { const sender = participantProfilesById.get(message.participantId) ?? buildParticipantProfile(message.participantId); @@ -391,7 +394,7 @@ export const LiveRoomChatPanel = ({
@@ -434,6 +437,9 @@ export const LiveRoomChatPanel = ({ senderName={senderName} reactionBusy={reactionBusy} hideQuickReactions={isMobile} + floatingTrayPlacement={ + messageIndex === 0 ? 'below' : 'above' + } onReactionAction={runReactionAction} />
diff --git a/packages/shared/src/components/liveRooms/LiveRoomChatReactions.tsx b/packages/shared/src/components/liveRooms/LiveRoomChatReactions.tsx index 7b0cd87be1b..2f13b6ed783 100644 --- a/packages/shared/src/components/liveRooms/LiveRoomChatReactions.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoomChatReactions.tsx @@ -1,6 +1,5 @@ import type { ReactElement } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import classNames from 'classnames'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { EmojiPicker } from '../fields/EmojiPicker'; import { IconSize } from '../Icon'; @@ -53,7 +52,6 @@ const FIRST_REACTION_BURST_DURATION_MS = 720; const FIRST_REACTION_PARTICLE_MAX_DELAY_MS = 90; const FIRST_REACTION_BURST_CLEAR_DELAY_MS = FIRST_REACTION_BURST_DURATION_MS + FIRST_REACTION_PARTICLE_MAX_DELAY_MS; -const MAX_REACTION_SLOTS = 5; export const getChatReactionGroups = ( message: LiveRoomChatEntry, @@ -293,6 +291,7 @@ interface LiveRoomChatReactionsProps { senderName: string; reactionBusy: string | null; hideQuickReactions?: boolean; + floatingTrayPlacement?: 'above' | 'below'; onReactionAction: ( messageId: string, reactionKey: string, @@ -308,15 +307,18 @@ export const LiveRoomChatReactions = ({ senderName, reactionBusy, hideQuickReactions = false, + floatingTrayPlacement = 'above', onReactionAction, }: LiveRoomChatReactionsProps): ReactElement | null => { const { firstReactionBurst, getPulseSignal } = useChatReactionAnimations(message); const reactionGroups = getChatReactionGroups(message, currentParticipantId); - const reactionKeys = new Set(reactionGroups.map((reaction) => reaction.key)); - const quickReactionKeys = LIVE_ROOM_QUICK_REACTION_EMOJIS.filter( - (reactionKey) => !reactionKeys.has(reactionKey), - ).slice(0, Math.max(0, MAX_REACTION_SLOTS - reactionGroups.length)); + const myReactionKeys = new Set( + reactionGroups + .filter((group) => group.isReactedByCurrentParticipant) + .map((group) => group.key), + ); + const quickReactionKeys = LIVE_ROOM_QUICK_REACTION_EMOJIS; const showQuickReactions = canChat && quickReactionKeys.length > 0; const hasActiveReactions = reactionGroups.length > 0; const baseReactionAnalytics = { @@ -333,77 +335,91 @@ export const LiveRoomChatReactions = ({ } const renderQuickReactionsTray = !hideQuickReactions; + const showFloatingTray = + renderQuickReactionsTray && (showQuickReactions || canChat); return ( -
- {firstReactionBurst ? ( - + <> + {hasActiveReactions ? ( +
+ {firstReactionBurst ? ( + + ) : null} + {reactionGroups.map((reaction) => { + const reactionKey = `${message.messageId}-${reaction.key}`; + + return ( + + onReactionAction( + message.messageId, + reaction.key, + { + ...baseReactionAnalytics, + source: 'active_chip', + }, + reaction.isReactedByCurrentParticipant, + ) + } + /> + ); + })} +
) : null} - {reactionGroups.map((reaction) => { - const reactionKey = `${message.messageId}-${reaction.key}`; - - return ( - - onReactionAction( - message.messageId, - reaction.key, - { - ...baseReactionAnalytics, - source: 'active_chip', - }, - reaction.isReactedByCurrentParticipant, - ) - } - /> - ); - })} - {renderQuickReactionsTray && (showQuickReactions || canChat) ? ( + {showFloatingTray ? ( ) : null} -
+ ); }; diff --git a/packages/shared/src/components/liveRooms/LiveRoomReactionsToolbar.tsx b/packages/shared/src/components/liveRooms/LiveRoomReactionsToolbar.tsx index 6554c99df06..dfead741938 100644 --- a/packages/shared/src/components/liveRooms/LiveRoomReactionsToolbar.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoomReactionsToolbar.tsx @@ -5,7 +5,6 @@ import { EmojiPicker } from '../fields/EmojiPicker'; import { PlusIcon } from '../icons'; import { IconSize } from '../Icon'; import { LIVE_ROOM_QUICK_REACTION_EMOJIS } from '../../lib/liveRoom/reactions'; -import { LiveRoomTooltipButton } from './LiveRoomTooltipButton'; interface LiveRoomReactionsToolbarProps { isBusy: (key: string) => boolean; @@ -20,20 +19,19 @@ export const LiveRoomReactionsToolbar = ({ onRequestLogin, onSendReaction, }: LiveRoomReactionsToolbarProps): ReactElement => ( -
+
{LIVE_ROOM_QUICK_REACTION_EMOJIS.map((emoji) => ( - - - + ))} ( - -
diff --git a/packages/shared/src/components/liveRooms/LiveRoomVideoTile.tsx b/packages/shared/src/components/liveRooms/LiveRoomVideoTile.tsx index 03c51bf890b..863a8af0405 100644 --- a/packages/shared/src/components/liveRooms/LiveRoomVideoTile.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoomVideoTile.tsx @@ -291,6 +291,14 @@ export const LiveRoomVideoTile = ({ onClick={handleFocusButtonClick} /> ) : null} + {isFocused ? ( + - ); - }) - : null} - { - if (!reactionKey) { - return; - } - - onReactionAction(message.messageId, reactionKey, { - ...baseReactionAnalytics, - source: 'custom_picker', - }); - }} - renderTrigger={({ isOpen, toggleOpen }) => ( - + ); + }) + : null} + { + if (!reactionKey) { + return; + } + + onReactionAction(message.messageId, reactionKey, { + ...baseReactionAnalytics, + source: 'custom_picker', + }); + }} + renderTrigger={({ isOpen, toggleOpen }) => ( +
) : null} From b885835cfdbcda7e83eac84308ed01488bc3a9c1 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Tue, 5 May 2026 17:36:30 +0300 Subject: [PATCH 3/3] test: update reaction tray spec for floating tray behavior --- .../components/liveRooms/LiveRoom.spec.tsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx b/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx index 83134e7384b..bde99012a89 100644 --- a/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx +++ b/packages/shared/src/components/liveRooms/LiveRoom.spec.tsx @@ -703,7 +703,7 @@ describe('LiveRoom', () => { expect(screen.getByRole('button', { name: 'Send' })).toBeInTheDocument(); }); - it('limits chat reaction shortcuts to the remaining slots when active reactions exist', async () => { + it('always shows all quick reactions and toggles a reacted emoji off when clicked', async () => { const sendChatMessageReaction = jest.fn().mockResolvedValue(undefined); const removeChatMessageReaction = jest.fn().mockResolvedValue(undefined); mockUseLiveRoomConnection.mockReturnValue( @@ -749,16 +749,11 @@ describe('LiveRoom', () => { renderLiveRoom(); - expect( - screen.getByRole('button', { - name: 'Remove 🔥 reaction from message from @speaker1', - }), - ).toHaveTextContent('2'); - expect( - screen.getAllByRole('button', { - name: /(?:React|Remove) .* (?:to|reaction from) message from @speaker1/, - }), - ).toHaveLength(5); + const fireRemoveButtons = screen.getAllByRole('button', { + name: 'Remove 🔥 reaction from message from @speaker1', + }); + expect(fireRemoveButtons).toHaveLength(2); + expect(fireRemoveButtons[0]).toHaveTextContent('2'); expect( screen.getByRole('button', { name: 'React 👀 to message from @speaker1', @@ -775,11 +770,7 @@ describe('LiveRoom', () => { }), ).toBeInTheDocument(); - fireEvent.click( - screen.getByRole('button', { - name: 'Remove 🔥 reaction from message from @speaker1', - }), - ); + fireEvent.click(fireRemoveButtons[0]); await waitFor(() => expect(removeChatMessageReaction).toHaveBeenCalledWith('message-1', '🔥'),