fix(room): replace manual emoji picker positioning with Popover
Manual getBoundingClientRect positioning caused the picker to appear at the far right of the room and shift content. Replaced with shadcn Popover which handles anchor positioning, flipping, and portal rendering automatically.
This commit is contained in:
parent
4767e1d692
commit
ef1adb663d
@ -6,9 +6,10 @@ import { parseFunctionCalls, type FunctionCall } from '@/lib/functionCallParser'
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AlertCircle, AlertTriangle, ChevronDown, ChevronUp, Copy, Edit2, Reply as ReplyIcon, Trash2, History, MoreHorizontal, MessageSquare } from 'lucide-react';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { SmilePlus } from 'lucide-react';
|
||||
import { useUser, useRoom } from '@/contexts';
|
||||
import { memo, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { memo, useMemo, useCallback, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ModelIcon } from './icon-match';
|
||||
import { FunctionCallBadge } from './FunctionCallBadge';
|
||||
@ -79,10 +80,7 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editContent, setEditContent] = useState(message.content);
|
||||
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
||||
const [showReactionPicker, setShowReactionPicker] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const reactionButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [reactionPickerPosition, setReactionPickerPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
const isAi = ['ai', 'system', 'tool'].includes(message.sender_type);
|
||||
const isSystem = message.sender_type === 'system';
|
||||
@ -121,20 +119,8 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
} catch (err) {
|
||||
console.warn('[RoomMessage] Failed to update reaction:', err);
|
||||
}
|
||||
setShowReactionPicker(false);
|
||||
}, [roomId, message.id, message.reactions, wsClient]);
|
||||
|
||||
const handleOpenReactionPicker = useCallback(() => {
|
||||
if (reactionButtonRef.current) {
|
||||
const rect = reactionButtonRef.current.getBoundingClientRect();
|
||||
setReactionPickerPosition({
|
||||
top: rect.bottom + 8, // 8px below the button
|
||||
left: rect.left + rect.width / 2,
|
||||
});
|
||||
}
|
||||
setShowReactionPicker(true);
|
||||
}, []);
|
||||
|
||||
const functionCalls = useMemo<FunctionCall[]>(
|
||||
() =>
|
||||
message.content_type === 'text' || message.content_type === 'Text'
|
||||
@ -408,16 +394,37 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
{!isEditing && !isRevoked && !isPending && (
|
||||
<div className="flex items-start gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{/* Add reaction */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
ref={reactionButtonRef}
|
||||
className="size-7 p-0 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
onClick={handleOpenReactionPicker}
|
||||
title="Add reaction"
|
||||
>
|
||||
<SmilePlus className="size-3.5" />
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 p-0 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
title="Add reaction"
|
||||
>
|
||||
<SmilePlus className="size-3.5" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent className="w-auto p-2" align="start" sideOffset={4}>
|
||||
<p className="mb-2 text-xs font-medium text-muted-foreground">Select emoji</p>
|
||||
<div className="grid grid-cols-8 gap-1">
|
||||
{COMMON_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleReaction(emoji)}
|
||||
className="size-7 p-0 text-base hover:bg-accent"
|
||||
title={emoji}
|
||||
>
|
||||
{emoji}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* Reply */}
|
||||
{onReply && (
|
||||
<Button
|
||||
@ -493,28 +500,6 @@ export const RoomMessageBubble = memo(function RoomMessageBubble({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Emoji picker overlay - positioned relative to the reaction button */}
|
||||
{showReactionPicker && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowReactionPicker(false)} />
|
||||
<div
|
||||
className="fixed z-50"
|
||||
style={{
|
||||
top: reactionPickerPosition?.top ?? '50%',
|
||||
left: reactionPickerPosition?.left ?? '50%',
|
||||
transform: reactionPickerPosition ? 'translateX(-50%)' : 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div className="rounded-lg border border-border bg-popover p-3 shadow-xl">
|
||||
<p className="mb-2 text-xs font-medium text-muted-foreground">Select emoji</p>
|
||||
<EmojiPicker onEmojiSelect={(emoji) => {
|
||||
handleReaction(emoji);
|
||||
setShowReactionPicker(false);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user