fix: chat UI
This commit is contained in:
@@ -81,7 +81,7 @@ function QuotedReply({
|
||||
export function MessageGroup({
|
||||
messages,
|
||||
mine,
|
||||
peer,
|
||||
sender,
|
||||
receipts,
|
||||
showAvatar,
|
||||
selfId,
|
||||
@@ -93,7 +93,9 @@ export function MessageGroup({
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
mine: boolean;
|
||||
peer?: ChatPeer | null;
|
||||
/** Who sent this group of messages. Undefined for Kautilya, system
|
||||
* messages, or a member who has since left. */
|
||||
sender?: ChatPeer;
|
||||
receipts: Receipts | null;
|
||||
/** Groups show an avatar; a DM's own side never does. */
|
||||
showAvatar: boolean;
|
||||
@@ -119,20 +121,31 @@ export function MessageGroup({
|
||||
>
|
||||
{!mine && showAvatar && (
|
||||
<UserAvatar
|
||||
src={peer?.avatar_url}
|
||||
name={peer?.name ?? "?"}
|
||||
src={sender?.avatar_url}
|
||||
name={sender?.name ?? "?"}
|
||||
className="size-7 shrink-0"
|
||||
textClassName="text-[10px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
// flex-1 rather than max-w: the column spans the row so a swipe can
|
||||
// start anywhere on it. min-w-0 lets long words wrap instead of
|
||||
// widening the row.
|
||||
className={cn(
|
||||
"flex max-w-[78%] flex-col gap-0.5",
|
||||
"flex min-w-0 flex-1 flex-col gap-0.5",
|
||||
mine ? "items-end" : "items-start",
|
||||
!mine && !showAvatar && "ml-9",
|
||||
)}
|
||||
>
|
||||
{/* Name the sender once per run of messages. An avatar alone is thin
|
||||
identification in a group — two people with similar photos, or
|
||||
none at all, are indistinguishable at 28px. */}
|
||||
{!mine && showAvatar && (
|
||||
<span className="px-1 text-[11px] font-medium text-muted-foreground">
|
||||
{sender?.name ?? "Unknown"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{messages.map((m, i) => (
|
||||
// A deleted message has nothing to quote, so it isn't swipeable.
|
||||
<MessageGestures
|
||||
@@ -143,62 +156,73 @@ export function MessageGroup({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"group/bubble relative px-3 py-2 text-sm",
|
||||
bubbleCorners(mine, i, messages.length),
|
||||
m.deleted_at
|
||||
? "bg-muted italic text-muted-foreground"
|
||||
: mine
|
||||
? "bg-primary text-primary-foreground dark:text-white"
|
||||
: "bg-muted text-foreground",
|
||||
"flex w-full",
|
||||
mine ? "justify-end" : "justify-start",
|
||||
)}
|
||||
>
|
||||
{m.reply_to && !m.deleted_at && (
|
||||
<QuotedReply
|
||||
reply={m.reply_to}
|
||||
mine={mine}
|
||||
selfId={selfId}
|
||||
nameFor={nameFor}
|
||||
/>
|
||||
)}
|
||||
{m.deleted_at ? (
|
||||
<span>This message was deleted</span>
|
||||
) : (
|
||||
<>
|
||||
{m.body && <MessageText body={m.body} entities={m.entities} />}
|
||||
<MessageAttachments attachments={m.attachments} />
|
||||
{/* An optimistic bubble for a captionless attachment has
|
||||
<div
|
||||
className={cn(
|
||||
"group/bubble relative max-w-[78%] px-3 py-2 text-sm",
|
||||
bubbleCorners(mine, i, messages.length),
|
||||
m.deleted_at
|
||||
? "bg-muted italic text-muted-foreground"
|
||||
: mine
|
||||
? "bg-primary text-primary-foreground dark:text-white"
|
||||
: "bg-muted text-foreground",
|
||||
)}
|
||||
>
|
||||
{m.reply_to && !m.deleted_at && (
|
||||
<QuotedReply
|
||||
reply={m.reply_to}
|
||||
mine={mine}
|
||||
selfId={selfId}
|
||||
nameFor={nameFor}
|
||||
/>
|
||||
)}
|
||||
{m.deleted_at ? (
|
||||
<span>This message was deleted</span>
|
||||
) : (
|
||||
<>
|
||||
{m.body && (
|
||||
<MessageText body={m.body} entities={m.entities} />
|
||||
)}
|
||||
<MessageAttachments attachments={m.attachments} />
|
||||
{/* An optimistic bubble for a captionless attachment has
|
||||
nothing to draw until the server answers — say so rather
|
||||
than render an empty box. */}
|
||||
{m.pending && !m.body && m.attachments.length === 0 && (
|
||||
<span className="italic opacity-80">Sending attachment…</span>
|
||||
)}
|
||||
{starredIds.has(m.id) && (
|
||||
<Star
|
||||
className="mt-1 size-3 fill-current opacity-70"
|
||||
aria-label="Starred"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{m.pending && !m.body && m.attachments.length === 0 && (
|
||||
<span className="italic opacity-80">
|
||||
Sending attachment…
|
||||
</span>
|
||||
)}
|
||||
{starredIds.has(m.id) && (
|
||||
<Star
|
||||
className="mt-1 size-3 fill-current opacity-70"
|
||||
aria-label="Starred"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Desktop affordance. Swiping is the mobile gesture, but a
|
||||
{/* Desktop affordance. Swiping is the mobile gesture, but a
|
||||
drag is not discoverable with a mouse — and a real button is
|
||||
also reachable by keyboard, which a gesture never is. */}
|
||||
{!m.deleted_at && !m.pending && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReply(m)}
|
||||
aria-label="Reply to this message"
|
||||
className={cn(
|
||||
"absolute top-1/2 hidden -translate-y-1/2 rounded-full border border-border bg-background p-1.5 text-muted-foreground shadow-sm",
|
||||
"opacity-0 transition-opacity group-hover/bubble:opacity-100 focus-visible:opacity-100",
|
||||
"hover:text-foreground sm:block",
|
||||
mine ? "-left-9" : "-right-9",
|
||||
)}
|
||||
>
|
||||
<Reply className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{!m.deleted_at && !m.pending && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReply(m)}
|
||||
aria-label="Reply to this message"
|
||||
className={cn(
|
||||
"absolute top-1/2 hidden -translate-y-1/2 rounded-full border border-border bg-background p-1.5 text-muted-foreground shadow-sm",
|
||||
"opacity-0 transition-opacity group-hover/bubble:opacity-100 focus-visible:opacity-100",
|
||||
"hover:text-foreground sm:block",
|
||||
mine ? "-left-9" : "-right-9",
|
||||
)}
|
||||
>
|
||||
<Reply className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</MessageGestures>
|
||||
))}
|
||||
@@ -223,8 +247,10 @@ export function MessageGroup({
|
||||
Not sent — retry
|
||||
</button>
|
||||
)}
|
||||
{mine && !last.deleted_at && !last.failed && (
|
||||
last.pending ? (
|
||||
{mine &&
|
||||
!last.deleted_at &&
|
||||
!last.failed &&
|
||||
(last.pending ? (
|
||||
<Clock
|
||||
role="img"
|
||||
aria-label="Sending"
|
||||
@@ -232,8 +258,7 @@ export function MessageGroup({
|
||||
/>
|
||||
) : (
|
||||
<MessageTicks state={tickState(last.seq, receipts)} />
|
||||
)
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user