Files
iios/packages/iios-messaging-ui/src/components/message-item.tsx
T
maaz519 3c5c6964e2 fix(messaging-ui): mail cards no longer clip + emoji picker portals above overflow (0.1.3)
- Mail reader: .miu-mail-msg gets flex:0 0 auto so cards keep their natural height
  in the scrolling column instead of being compressed + clipped by overflow:hidden.
- Reaction/emoji picker renders through a PopoverPortal to <body> (positioned +
  themed) so it floats above the message list instead of being cut by the scroll
  container's overflow. 72 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 03:10:31 +05:30

112 lines
3.8 KiB
TypeScript

import { useRef, useState } from 'react';
import { highlightMentions } from '../mentions';
import { PopoverPortal } from './popover-portal';
import type { Attachment } from '../types';
import type { UiMessage } from '../hooks/use-messages';
const REACTION_EMOJIS = ['👍', '❤️', '😂', '🎉', '👀'];
const isImage = (mime: string): boolean => mime.startsWith('image/');
function AttachmentView({ att }: { att: Attachment }) {
if (isImage(att.mime)) {
return (
<a href={att.url} target="_blank" rel="noreferrer" className="miu-att-img-link">
<img src={att.url} alt={att.name} className="miu-att-img" />
</a>
);
}
return (
<a href={att.url} target="_blank" rel="noreferrer" className="miu-att-file">
📎 {att.name}
</a>
);
}
/** One rendered message: bubble (with @mention highlighting), attachment, reactions, seen tick,
* and — in the main thread only — a thread/reply affordance. */
export function MessageItem({
message,
memberNames,
canReact,
onReact,
seen,
replyCount,
onOpenThread,
}: {
message: UiMessage;
memberNames: string[];
canReact: boolean;
onReact: (messageId: string, emoji: string) => void;
seen: boolean;
/** Present only in the main thread (not inside the pane). undefined => no thread affordance. */
replyCount?: number;
onOpenThread?: (messageId: string) => void;
}) {
const [pickerOpen, setPickerOpen] = useState(false);
const reactBtnRef = useRef<HTMLButtonElement>(null);
const m = message;
return (
<div className={`miu-msg${message.mine ? ' is-mine' : ''}${message.pending ? ' is-pending' : ''}`}>
<div className="miu-bubble-row">
<div className="miu-bubble">
{m.text ? highlightMentions(m.text, memberNames) : null}
{m.attachment ? <AttachmentView att={m.attachment} /> : null}
</div>
<div className="miu-msg-actions">
{canReact ? (
<div className="miu-react-wrap">
<button ref={reactBtnRef} type="button" className="miu-react-btn" title="React" onClick={() => setPickerOpen((p) => !p)}>
🙂
</button>
{pickerOpen ? (
<PopoverPortal anchorRef={reactBtnRef} onClose={() => setPickerOpen(false)}>
<div className="miu-react-picker">
{REACTION_EMOJIS.map((e) => (
<button
key={e}
type="button"
className="miu-react-emoji"
onClick={() => {
onReact(m.id, e);
setPickerOpen(false);
}}
>
{e}
</button>
))}
</div>
</PopoverPortal>
) : null}
</div>
) : null}
{onOpenThread ? (
<button type="button" className="miu-react-btn" title="Reply in thread" onClick={() => onOpenThread(m.id)}>
💬
</button>
) : null}
</div>
</div>
{m.reactions && m.reactions.length > 0 ? (
<div className="miu-reactions">
{m.reactions.map((r) => (
<button key={r.emoji} type="button" className={`miu-reaction${r.mine ? ' is-mine' : ''}`} onClick={() => canReact && onReact(m.id, r.emoji)}>
{r.emoji} {r.count}
</button>
))}
</div>
) : null}
{replyCount !== undefined && replyCount > 0 && onOpenThread ? (
<button type="button" className="miu-thread-link" onClick={() => onOpenThread(m.id)}>
💬 {replyCount} {replyCount === 1 ? 'reply' : 'replies'}
</button>
) : null}
{message.mine && seen ? <span className="miu-seen">Seen</span> : null}
</div>
);
}