mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
chore: improve chat message layout to left-aligned design (#309)
This commit is contained in:
@@ -73,11 +73,11 @@ export function MessageActionBar({
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-36 overflow-hidden rounded-full border border-border/70 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-all duration-150 ease-out",
|
||||
"translate-y-0 opacity-100 sm:max-w-0 sm:translate-y-1 sm:opacity-0",
|
||||
"sm:group-hover/message:max-w-36 sm:group-hover/message:translate-y-0 sm:group-hover/message:opacity-100",
|
||||
"sm:group-focus-within/message:max-w-36 sm:group-focus-within/message:translate-y-0 sm:group-focus-within/message:opacity-100",
|
||||
"translate-y-0 opacity-100 sm:max-w-0 sm:border-0 sm:shadow-none sm:translate-y-1 sm:opacity-0",
|
||||
"sm:group-hover/message:max-w-36 sm:group-hover/message:border sm:group-hover/message:border-border/70 sm:group-hover/message:shadow-sm sm:group-hover/message:translate-y-0 sm:group-hover/message:opacity-100",
|
||||
"sm:group-focus-within/message:max-w-36 sm:group-focus-within/message:border sm:group-focus-within/message:border-border/70 sm:group-focus-within/message:shadow-sm sm:group-focus-within/message:translate-y-0 sm:group-focus-within/message:opacity-100",
|
||||
isReplyingToMessage || isReactionPickerOpen
|
||||
? "sm:max-w-36 sm:translate-y-0 sm:opacity-100"
|
||||
? "sm:max-w-36 sm:border sm:border-border/70 sm:shadow-sm sm:translate-y-0 sm:opacity-100"
|
||||
: "",
|
||||
)}
|
||||
data-testid={`message-action-bar-${message.id}`}
|
||||
|
||||
@@ -496,7 +496,7 @@ export function MessageComposer({
|
||||
|
||||
return (
|
||||
<footer className="border-t border-border/80 bg-background p-4">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-3">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<form
|
||||
className="relative rounded-2xl border border-input bg-card px-3 py-4 shadow-sm sm:px-4"
|
||||
data-testid="message-composer"
|
||||
|
||||
@@ -103,7 +103,7 @@ export const MessageRow = React.memo(
|
||||
return (
|
||||
<Markdown
|
||||
channelNames={channelNames}
|
||||
className="max-w-3xl"
|
||||
className="max-w-full"
|
||||
content={message.body}
|
||||
imetaByUrl={imetaByUrl}
|
||||
mentionNames={mentionNames}
|
||||
@@ -167,7 +167,7 @@ export const MessageRow = React.memo(
|
||||
|
||||
<article
|
||||
className={cn(
|
||||
"group/message flex items-start gap-2.5 rounded-2xl px-2 py-1 transition-colors",
|
||||
"group/message flex items-start gap-2.5 rounded-2xl px-2 py-1.5 transition-colors",
|
||||
highlighted ? "bg-primary/10 ring-1 ring-primary/30" : "",
|
||||
activeReplyTargetId === message.id
|
||||
? "bg-muted/60 ring-1 ring-border"
|
||||
@@ -176,16 +176,13 @@ export const MessageRow = React.memo(
|
||||
data-message-id={message.id}
|
||||
data-testid="message-row"
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{isBotInstance(message.role) ? (
|
||||
<BotIdenticon
|
||||
value={message.author}
|
||||
size={20}
|
||||
className="rounded"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex shrink-0 items-start">
|
||||
{message.pubkey ? (
|
||||
<UserProfilePopover pubkey={message.pubkey}>
|
||||
<UserProfilePopover
|
||||
pubkey={message.pubkey}
|
||||
role={message.role}
|
||||
botIdenticonValue={message.author}
|
||||
>
|
||||
<button
|
||||
className="shrink-0 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
@@ -193,6 +190,7 @@ export const MessageRow = React.memo(
|
||||
<UserAvatar
|
||||
accent={message.accent}
|
||||
avatarUrl={message.avatarUrl ?? null}
|
||||
className="!h-[42px] !w-[42px]"
|
||||
displayName={message.author}
|
||||
testId="message-avatar"
|
||||
/>
|
||||
@@ -202,17 +200,21 @@ export const MessageRow = React.memo(
|
||||
<UserAvatar
|
||||
accent={message.accent}
|
||||
avatarUrl={message.avatarUrl ?? null}
|
||||
className="shrink-0"
|
||||
className="shrink-0 !h-[42px] !w-[42px]"
|
||||
displayName={message.author}
|
||||
testId="message-avatar"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{message.pubkey ? (
|
||||
<UserProfilePopover pubkey={message.pubkey}>
|
||||
<UserProfilePopover
|
||||
pubkey={message.pubkey}
|
||||
role={message.role}
|
||||
botIdenticonValue={message.author}
|
||||
>
|
||||
<button
|
||||
className="truncate rounded text-sm font-semibold tracking-tight hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
@@ -225,29 +227,41 @@ export const MessageRow = React.memo(
|
||||
{message.author}
|
||||
</h3>
|
||||
)}
|
||||
{message.personaDisplayName ? (
|
||||
{isBotInstance(message.role) ? (
|
||||
<BotIdenticon
|
||||
value={message.author}
|
||||
size={14}
|
||||
className="shrink-0 rounded-sm"
|
||||
/>
|
||||
) : null}
|
||||
{message.personaDisplayName &&
|
||||
message.personaDisplayName !== message.author ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{message.personaDisplayName}
|
||||
</span>
|
||||
) : message.role ? (
|
||||
) : message.role && !isBotInstance(message.role) ? (
|
||||
<p className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{message.role}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
message={message}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReactionSelect={
|
||||
canToggleReactions ? handleReactionSelect : undefined
|
||||
}
|
||||
onReply={onReply}
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactionPending={reactionPending}
|
||||
reactions={reactions}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2">
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
message={message}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReactionSelect={
|
||||
canToggleReactions ? handleReactionSelect : undefined
|
||||
}
|
||||
onReply={onReply}
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactionPending={reactionPending}
|
||||
reactions={reactions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{message.pending ? (
|
||||
<p className="font-medium uppercase tracking-[0.14em] text-primary/80">
|
||||
Sending
|
||||
|
||||
@@ -110,10 +110,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
onScroll={syncScrollState}
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
<div
|
||||
className="mx-auto flex w-full max-w-4xl flex-col gap-2"
|
||||
ref={contentRef}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-3 px-4" ref={contentRef}>
|
||||
<div ref={topSentinelRef} aria-hidden className="h-px" />
|
||||
|
||||
{isFetchingOlder ? (
|
||||
|
||||
@@ -76,7 +76,7 @@ export function TypingIndicatorRow({
|
||||
className="bg-background/95 px-4 py-2 sm:px-6"
|
||||
data-testid="message-typing-indicator"
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-4xl items-center gap-2">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
{typingPubkeys.map((pubkey, index) => {
|
||||
const profile = profiles?.[pubkey];
|
||||
|
||||
@@ -10,10 +10,15 @@ import { formatRelativeTime } from "@/features/forum/lib/time";
|
||||
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { BotIdenticon } from "@/features/messages/ui/BotIdenticon";
|
||||
|
||||
type UserProfilePopoverProps = {
|
||||
children: React.ReactNode;
|
||||
pubkey: string;
|
||||
/** When set to "bot", a BotIdenticon badge renders next to the display name. */
|
||||
role?: string;
|
||||
/** Value used to generate the BotIdenticon glyph (typically the author name). */
|
||||
botIdenticonValue?: string;
|
||||
};
|
||||
|
||||
function truncatePubkey(pubkey: string) {
|
||||
@@ -27,6 +32,8 @@ function truncatePubkey(pubkey: string) {
|
||||
export function UserProfilePopover({
|
||||
children,
|
||||
pubkey,
|
||||
role,
|
||||
botIdenticonValue,
|
||||
}: UserProfilePopoverProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [showAllNotes, setShowAllNotes] = React.useState(false);
|
||||
@@ -64,9 +71,18 @@ export function UserProfilePopover({
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">
|
||||
{profile?.displayName ?? truncatePubkey(pubkey)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="truncate text-sm font-semibold">
|
||||
{profile?.displayName ?? truncatePubkey(pubkey)}
|
||||
</p>
|
||||
{role === "bot" && botIdenticonValue ? (
|
||||
<BotIdenticon
|
||||
value={botIdenticonValue}
|
||||
size={20}
|
||||
className="shrink-0 rounded"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{profile?.nip05Handle ? (
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{profile.nip05Handle}
|
||||
|
||||
@@ -8,7 +8,7 @@ type UserAvatarSize = "xs" | "sm" | "md";
|
||||
const sizeClasses: Record<UserAvatarSize, string> = {
|
||||
xs: "h-5 w-5 text-[8px]",
|
||||
sm: "h-6 w-6 text-[9px]",
|
||||
md: "h-8 w-8 text-[11px]",
|
||||
md: "h-10 w-10 text-xs",
|
||||
};
|
||||
|
||||
type UserAvatarProps = {
|
||||
|
||||
Reference in New Issue
Block a user