Skip to content

Commit 8881c3c

Browse files
authored
fix: standup feedback round (#5990)
1 parent 29f2bf7 commit 8881c3c

7 files changed

Lines changed: 195 additions & 170 deletions

File tree

packages/shared/src/components/liveRooms/CreateLiveRoomForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import Link from '../utilities/Link';
3131
import { LogEvent } from '../../lib/log';
3232
import styles from './CreateLiveRoomForm.module.css';
3333

34-
const DEFAULT_FREE_FOR_ALL_SPEAKER_LIMIT = 4;
34+
const DEFAULT_FREE_FOR_ALL_SPEAKER_LIMIT = 100;
3535
const DEFAULT_SCHEDULE_DELAY_MS = 30 * 60 * 1000;
3636
const DESCRIPTION_MAX_LENGTH = 4000;
3737

packages/shared/src/components/liveRooms/LiveRoom.spec.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ describe('LiveRoom', () => {
703703
expect(screen.getByRole('button', { name: 'Send' })).toBeInTheDocument();
704704
});
705705

706-
it('limits chat reaction shortcuts to the remaining slots when active reactions exist', async () => {
706+
it('always shows all quick reactions and toggles a reacted emoji off when clicked', async () => {
707707
const sendChatMessageReaction = jest.fn().mockResolvedValue(undefined);
708708
const removeChatMessageReaction = jest.fn().mockResolvedValue(undefined);
709709
mockUseLiveRoomConnection.mockReturnValue(
@@ -749,16 +749,11 @@ describe('LiveRoom', () => {
749749

750750
renderLiveRoom();
751751

752-
expect(
753-
screen.getByRole('button', {
754-
name: 'Remove 🔥 reaction from message from @speaker1',
755-
}),
756-
).toHaveTextContent('2');
757-
expect(
758-
screen.getAllByRole('button', {
759-
name: /(?:React|Remove) .* (?:to|reaction from) message from @speaker1/,
760-
}),
761-
).toHaveLength(5);
752+
const fireRemoveButtons = screen.getAllByRole('button', {
753+
name: 'Remove 🔥 reaction from message from @speaker1',
754+
});
755+
expect(fireRemoveButtons).toHaveLength(2);
756+
expect(fireRemoveButtons[0]).toHaveTextContent('2');
762757
expect(
763758
screen.getByRole('button', {
764759
name: 'React 👀 to message from @speaker1',
@@ -775,11 +770,7 @@ describe('LiveRoom', () => {
775770
}),
776771
).toBeInTheDocument();
777772

778-
fireEvent.click(
779-
screen.getByRole('button', {
780-
name: 'Remove 🔥 reaction from message from @speaker1',
781-
}),
782-
);
773+
fireEvent.click(fireRemoveButtons[0]);
783774

784775
await waitFor(() =>
785776
expect(removeChatMessageReaction).toHaveBeenCalledWith('message-1', '🔥'),

packages/shared/src/components/liveRooms/LiveRoomChatPanel.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ export const LiveRoomChatPanel = ({
243243
const [pickerMessageId, setPickerMessageId] = useState<string | null>(null);
244244
const longPressHandlers = useTouchLongPress<string>({
245245
enabled: isMobile && canChat,
246-
onLongPress: setPickerMessageId,
246+
onLongPress: (messageId) => {
247+
window.getSelection()?.removeAllRanges();
248+
setPickerMessageId(messageId);
249+
},
247250
});
248251

249252
const pickerMessage = pickerMessageId
@@ -350,7 +353,7 @@ export const LiveRoomChatPanel = ({
350353
<div
351354
ref={scrollRef}
352355
onScroll={handleScroll}
353-
className="flex flex-1 flex-col gap-3 overflow-y-auto p-3"
356+
className="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2"
354357
>
355358
{chatMessages.length === 0 ? (
356359
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center">
@@ -370,7 +373,7 @@ export const LiveRoomChatPanel = ({
370373
</Typography>
371374
</div>
372375
) : (
373-
chatMessages.map((message) => {
376+
chatMessages.map((message, messageIndex) => {
374377
const sender =
375378
participantProfilesById.get(message.participantId) ??
376379
buildParticipantProfile(message.participantId);
@@ -391,7 +394,7 @@ export const LiveRoomChatPanel = ({
391394
<article
392395
key={message.messageId}
393396
className={classNames(
394-
'group flex items-start gap-2 px-1 py-1.5',
397+
'group relative flex items-start gap-2 px-1 py-1',
395398
isMobile && 'select-none [-webkit-touch-callout:none]',
396399
)}
397400
onTouchStart={(event) =>
@@ -434,6 +437,9 @@ export const LiveRoomChatPanel = ({
434437
senderName={senderName}
435438
reactionBusy={reactionBusy}
436439
hideQuickReactions={isMobile}
440+
floatingTrayPlacement={
441+
messageIndex === 0 ? 'below' : 'above'
442+
}
437443
onReactionAction={runReactionAction}
438444
/>
439445
</div>

packages/shared/src/components/liveRooms/LiveRoomChatReactions.tsx

Lines changed: 132 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ReactElement } from 'react';
22
import React, { useEffect, useMemo, useRef, useState } from 'react';
3-
import classNames from 'classnames';
43
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
54
import { EmojiPicker } from '../fields/EmojiPicker';
65
import { IconSize } from '../Icon';
@@ -53,7 +52,6 @@ const FIRST_REACTION_BURST_DURATION_MS = 720;
5352
const FIRST_REACTION_PARTICLE_MAX_DELAY_MS = 90;
5453
const FIRST_REACTION_BURST_CLEAR_DELAY_MS =
5554
FIRST_REACTION_BURST_DURATION_MS + FIRST_REACTION_PARTICLE_MAX_DELAY_MS;
56-
const MAX_REACTION_SLOTS = 5;
5755

5856
export const getChatReactionGroups = (
5957
message: LiveRoomChatEntry,
@@ -293,6 +291,7 @@ interface LiveRoomChatReactionsProps {
293291
senderName: string;
294292
reactionBusy: string | null;
295293
hideQuickReactions?: boolean;
294+
floatingTrayPlacement?: 'above' | 'below';
296295
onReactionAction: (
297296
messageId: string,
298297
reactionKey: string,
@@ -308,15 +307,18 @@ export const LiveRoomChatReactions = ({
308307
senderName,
309308
reactionBusy,
310309
hideQuickReactions = false,
310+
floatingTrayPlacement = 'above',
311311
onReactionAction,
312312
}: LiveRoomChatReactionsProps): ReactElement | null => {
313313
const { firstReactionBurst, getPulseSignal } =
314314
useChatReactionAnimations(message);
315315
const reactionGroups = getChatReactionGroups(message, currentParticipantId);
316-
const reactionKeys = new Set(reactionGroups.map((reaction) => reaction.key));
317-
const quickReactionKeys = LIVE_ROOM_QUICK_REACTION_EMOJIS.filter(
318-
(reactionKey) => !reactionKeys.has(reactionKey),
319-
).slice(0, Math.max(0, MAX_REACTION_SLOTS - reactionGroups.length));
316+
const myReactionKeys = new Set(
317+
reactionGroups
318+
.filter((group) => group.isReactedByCurrentParticipant)
319+
.map((group) => group.key),
320+
);
321+
const quickReactionKeys = LIVE_ROOM_QUICK_REACTION_EMOJIS;
320322
const showQuickReactions = canChat && quickReactionKeys.length > 0;
321323
const hasActiveReactions = reactionGroups.length > 0;
322324
const baseReactionAnalytics = {
@@ -333,117 +335,136 @@ export const LiveRoomChatReactions = ({
333335
}
334336

335337
const renderQuickReactionsTray = !hideQuickReactions;
338+
const showFloatingTray =
339+
renderQuickReactionsTray && (showQuickReactions || canChat);
336340

337341
return (
338-
<div
339-
className={classNames(
340-
'relative mt-1 flex flex-wrap items-center gap-1 transition-opacity',
341-
!hasActiveReactions
342-
? 'opacity-100 tablet:opacity-0 tablet:group-focus-within:opacity-100 tablet:group-hover:opacity-100'
343-
: 'opacity-100',
344-
)}
345-
>
346-
{firstReactionBurst ? (
347-
<FirstReactionBurst
348-
emoji={firstReactionBurst.emoji}
349-
signal={firstReactionBurst.signal}
350-
/>
342+
<>
343+
{hasActiveReactions ? (
344+
<div className="relative mt-1 flex flex-wrap items-center gap-1">
345+
{firstReactionBurst ? (
346+
<FirstReactionBurst
347+
emoji={firstReactionBurst.emoji}
348+
signal={firstReactionBurst.signal}
349+
/>
350+
) : null}
351+
{reactionGroups.map((reaction) => {
352+
const reactionKey = `${message.messageId}-${reaction.key}`;
353+
354+
return (
355+
<ChatReactionChip
356+
key={reaction.key}
357+
emoji={reaction.key}
358+
count={reaction.count}
359+
ariaLabel={`${
360+
reaction.isReactedByCurrentParticipant ? 'Remove' : 'React'
361+
} ${reaction.key} ${
362+
reaction.isReactedByCurrentParticipant
363+
? 'reaction from'
364+
: 'to'
365+
} message from ${senderName}`}
366+
disabled={!canChat || !!reactionBusy}
367+
isSending={reactionBusy === reactionKey}
368+
pulseSignal={getPulseSignal(reaction.key)}
369+
onClick={() =>
370+
onReactionAction(
371+
message.messageId,
372+
reaction.key,
373+
{
374+
...baseReactionAnalytics,
375+
source: 'active_chip',
376+
},
377+
reaction.isReactedByCurrentParticipant,
378+
)
379+
}
380+
/>
381+
);
382+
})}
383+
</div>
351384
) : null}
352-
{reactionGroups.map((reaction) => {
353-
const reactionKey = `${message.messageId}-${reaction.key}`;
354-
355-
return (
356-
<ChatReactionChip
357-
key={reaction.key}
358-
emoji={reaction.key}
359-
count={reaction.count}
360-
ariaLabel={`${
361-
reaction.isReactedByCurrentParticipant ? 'Remove' : 'React'
362-
} ${reaction.key} ${
363-
reaction.isReactedByCurrentParticipant ? 'reaction from' : 'to'
364-
} message from ${senderName}`}
365-
disabled={!canChat || !!reactionBusy}
366-
isSending={reactionBusy === reactionKey}
367-
pulseSignal={getPulseSignal(reaction.key)}
368-
onClick={() =>
369-
onReactionAction(
370-
message.messageId,
371-
reaction.key,
372-
{
373-
...baseReactionAnalytics,
374-
source: 'active_chip',
375-
},
376-
reaction.isReactedByCurrentParticipant,
377-
)
378-
}
379-
/>
380-
);
381-
})}
382-
{renderQuickReactionsTray && (showQuickReactions || canChat) ? (
385+
{showFloatingTray ? (
383386
<div
384-
key={hasActiveReactions ? 'active-reactions' : 'empty-reactions'}
385-
className={classNames(
386-
'flex flex-wrap items-center gap-1 transition-opacity',
387-
hasActiveReactions &&
388-
'opacity-100 tablet:opacity-0 tablet:group-focus-within:opacity-100 tablet:group-hover:opacity-100',
389-
)}
387+
className={
388+
floatingTrayPlacement === 'below'
389+
? 'absolute right-2 top-full z-1 hidden group-focus-within:flex group-hover:flex'
390+
: 'absolute bottom-full right-2 z-1 hidden group-focus-within:flex group-hover:flex'
391+
}
390392
>
391-
{showQuickReactions
392-
? quickReactionKeys.map((reactionKey) => {
393-
const busyKey = `${message.messageId}-${reactionKey}`;
394-
395-
return (
396-
<button
397-
key={reactionKey}
398-
type="button"
399-
className="flex size-6 items-center justify-center rounded-8 border border-border-subtlest-tertiary bg-surface-float text-sm leading-none text-text-secondary hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50"
400-
aria-label={`React ${reactionKey} to message from ${senderName}`}
401-
disabled={!!reactionBusy}
402-
onClick={() =>
403-
onReactionAction(message.messageId, reactionKey, {
404-
...baseReactionAnalytics,
405-
source: 'quick_shortcut',
406-
})
407-
}
408-
>
409-
<span>{reactionKey}</span>
410-
{reactionBusy === busyKey ? (
411-
<span className="sr-only">Sending</span>
412-
) : null}
413-
</button>
414-
);
415-
})
416-
: null}
417-
<EmojiPicker
418-
value=""
419-
label={null}
420-
className="shrink-0"
421-
onChange={(reactionKey) => {
422-
if (!reactionKey) {
423-
return;
424-
}
425-
426-
onReactionAction(message.messageId, reactionKey, {
427-
...baseReactionAnalytics,
428-
source: 'custom_picker',
429-
});
430-
}}
431-
renderTrigger={({ isOpen, toggleOpen }) => (
432-
<Button
433-
type="button"
434-
size={ButtonSize.XSmall}
435-
variant={isOpen ? ButtonVariant.Primary : ButtonVariant.Float}
436-
className="!size-6 shrink-0"
437-
icon={<PlusIcon size={IconSize.Size16} />}
438-
aria-label={`Custom reaction to message from ${senderName}`}
439-
aria-expanded={isOpen}
440-
disabled={!!reactionBusy}
441-
onClick={toggleOpen}
442-
/>
443-
)}
444-
/>
393+
<div className="flex items-center gap-0.5 rounded-12 border border-border-subtlest-tertiary bg-background-default p-0.5 shadow-2">
394+
{showQuickReactions
395+
? quickReactionKeys.map((reactionKey) => {
396+
const busyKey = `${message.messageId}-${reactionKey}`;
397+
const isReacted = myReactionKeys.has(reactionKey);
398+
399+
return (
400+
<button
401+
key={reactionKey}
402+
type="button"
403+
className={
404+
isReacted
405+
? 'flex size-6 items-center justify-center rounded-8 bg-action-upvote-float text-sm leading-none text-action-upvote-default hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50'
406+
: 'flex size-6 items-center justify-center rounded-8 text-sm leading-none text-text-secondary hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50'
407+
}
408+
aria-label={`${
409+
isReacted ? 'Remove' : 'React'
410+
} ${reactionKey} ${
411+
isReacted ? 'reaction from' : 'to'
412+
} message from ${senderName}`}
413+
aria-pressed={isReacted}
414+
disabled={!!reactionBusy}
415+
onClick={() =>
416+
onReactionAction(
417+
message.messageId,
418+
reactionKey,
419+
{
420+
...baseReactionAnalytics,
421+
source: 'quick_shortcut',
422+
},
423+
isReacted,
424+
)
425+
}
426+
>
427+
<span>{reactionKey}</span>
428+
{reactionBusy === busyKey ? (
429+
<span className="sr-only">Sending</span>
430+
) : null}
431+
</button>
432+
);
433+
})
434+
: null}
435+
<EmojiPicker
436+
value=""
437+
label={null}
438+
className="shrink-0"
439+
onChange={(reactionKey) => {
440+
if (!reactionKey) {
441+
return;
442+
}
443+
444+
onReactionAction(message.messageId, reactionKey, {
445+
...baseReactionAnalytics,
446+
source: 'custom_picker',
447+
});
448+
}}
449+
renderTrigger={({ isOpen, toggleOpen }) => (
450+
<Button
451+
type="button"
452+
size={ButtonSize.XSmall}
453+
variant={
454+
isOpen ? ButtonVariant.Primary : ButtonVariant.Tertiary
455+
}
456+
className="!size-6 shrink-0"
457+
icon={<PlusIcon size={IconSize.Size16} />}
458+
aria-label={`Custom reaction to message from ${senderName}`}
459+
aria-expanded={isOpen}
460+
disabled={!!reactionBusy}
461+
onClick={toggleOpen}
462+
/>
463+
)}
464+
/>
465+
</div>
445466
</div>
446467
) : null}
447-
</div>
468+
</>
448469
);
449470
};

0 commit comments

Comments
 (0)