fix(desktop): scope relaxed spacing to threads

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-08-07 11:15:08 -07:00
parent e6ad01d29c
commit 610bc64d5f
6 changed files with 48 additions and 32 deletions

View File

@@ -24,7 +24,7 @@ export const THREAD_FOCUS_SLIVER_WIDTH_PX = 72;
* with auto horizontal margins so the reading measure stays comfortable no
* matter how wide the window gets.
*/
export const THREAD_FOCUS_COLUMN_MAX_WIDTH_PX = 880;
export const THREAD_FOCUS_COLUMN_MAX_WIDTH_PX = 820;
/**
* Horizontal distance the focus drawer travels on enter/exit.

View File

@@ -6,15 +6,13 @@
/** Inline gutter around thread message rows. */
export const THREAD_PANEL_MESSAGE_GUTTER_CLASS = "px-4";
/** Restores the original message gutter inside the expanded focus overlay. */
export const THREAD_FOCUS_MESSAGE_GUTTER_CLASS = "px-2";
/** Inline gutter around the thread composer and its activity row. */
export const THREAD_PANEL_COMPOSER_GUTTER_CLASS = "px-5";
/**
* Centers the reading column when a `columnMaxWidthPx` is supplied (focus-mode
* drawer). `px-10` is the original inline gutter between the column and drawer
* edges; the max-width itself is applied inline since it is caller-provided.
* drawer). The responsive gutter keeps compact focus drawers usable while
* preserving generous whitespace at desktop widths. The max-width itself is
* applied inline since it is a caller-provided value.
*/
export const THREAD_PANEL_COLUMN_CLASS = "mx-auto w-full px-10";
export const THREAD_PANEL_COLUMN_CLASS = "mx-auto w-full px-6 sm:px-10";

View File

@@ -12,7 +12,10 @@ import { useKnownAgentPubkeys } from "@/features/agents/useKnownAgentPubkeys";
import { HuddleAttachment } from "@/features/huddle/components/HuddleAttachment";
import { MessageReactions } from "@/features/messages/ui/MessageReactions";
import { useReactionHandler } from "@/features/messages/ui/useReactionHandler";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import {
formatOwnerLabel,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { useRemindLater } from "@/features/reminders/ui/RemindMeLaterProvider";
import {
@@ -81,6 +84,7 @@ export const MessageRow = React.memo(
isUnread,
layoutVariant = "default",
message,
currentPubkey,
onCollapseDepthGuide,
onCollapseDepthGuideHoverChange,
onCollapseDescendants,
@@ -120,6 +124,7 @@ export const MessageRow = React.memo(
isUnread?: boolean;
layoutVariant?: "default" | "thread-reply";
message: TimelineMessage;
currentPubkey?: string;
onCollapseDepthGuide?: (message: TimelineMessage) => void;
onCollapseDepthGuideHoverChange?: (
message: TimelineMessage,
@@ -209,11 +214,11 @@ export const MessageRow = React.memo(
},
[knownAgentPubkeys, profiles],
);
const profilePopoverRole =
const displayedAuthorIsAgent =
message.role === "bot" ||
(message.pubkey && isKnownAgentPubkey(message.pubkey))
? "bot"
: message.role;
message.isAgent === true ||
Boolean(message.pubkey && isKnownAgentPubkey(message.pubkey));
const profilePopoverRole = displayedAuthorIsAgent ? "bot" : message.role;
const agentMentionPubkeysByName = React.useMemo(() => {
if (!mentionPubkeysByName) {
return undefined;
@@ -246,7 +251,8 @@ export const MessageRow = React.memo(
message.body,
message.tags,
);
const bodyOffsetClass = "mt-1";
const bodyOffsetClass =
layoutVariant === "thread-reply" || emojiOnly ? "mt-1" : "-mt-0.5";
const { nonDmChannelNames: channelNames } = useChannelNavigation();
const openVideoReviewAt = useOpenVideoReviewAt();
@@ -367,7 +373,9 @@ export const MessageRow = React.memo(
<Markdown
channelNames={channelNames}
className={cn(
"max-w-full leading-relaxed [&>p+p]:mt-2 [&>ol]:space-y-1.5 [&>ul]:space-y-1.5",
"max-w-full text-sm",
layoutVariant === "thread-reply" &&
"leading-relaxed [&>p+p]:mt-2 [&>ol]:space-y-1.5 [&>ul]:space-y-1.5",
emojiOnly &&
"text-4xl leading-tight [&_p]:leading-tight [&_img[data-custom-emoji]]:h-[1.45em] [&_img[data-custom-emoji]]:align-middle [&_button:has(img[data-custom-emoji])]:align-middle",
)}
@@ -503,10 +511,18 @@ export const MessageRow = React.memo(
) : (
<MessageAuthorText as="h3">{message.author}</MessageAuthorText>
);
const agentOwnerNode = message.isAgent ? (
const agentOwnerPubkey =
message.ownerPubkey ??
(message.pubkey
? profiles?.[normalizePubkey(message.pubkey)]?.ownerPubkey
: null);
const agentOwnerLabel =
message.ownerLabel ??
formatOwnerLabel(agentOwnerPubkey, currentPubkey, profiles);
const agentOwnerNode = displayedAuthorIsAgent ? (
<MessageAgentOwner
ownerLabel={message.ownerLabel}
ownerPubkey={message.ownerPubkey}
ownerLabel={agentOwnerLabel}
ownerPubkey={agentOwnerPubkey}
/>
) : null;
@@ -823,7 +839,7 @@ export const MessageRow = React.memo(
className={cn(
"group/message relative z-10 rounded-2xl transition-colors",
playEntrance && "motion-enter-conversation",
isThreadReplyLayout ? "py-2" : "py-1.5",
isThreadReplyLayout ? "py-2" : "py-1",
hoverBackground
? "mx-1 px-2 hover:bg-muted/50 focus-within:bg-muted/50"
: isThreadReplyLayout
@@ -872,6 +888,7 @@ export const MessageRow = React.memo(
prev.message.isAgent === next.message.isAgent &&
prev.message.ownerPubkey === next.message.ownerPubkey &&
prev.message.ownerLabel === next.message.ownerLabel &&
prev.currentPubkey === next.currentPubkey &&
prev.message.avatarUrl === next.message.avatarUrl &&
prev.message.accent === next.message.accent &&
prev.message.time === next.message.time &&

View File

@@ -34,7 +34,6 @@ import {
AuxiliaryPanelTitle,
} from "@/shared/layout/AuxiliaryPanel";
import {
THREAD_FOCUS_MESSAGE_GUTTER_CLASS,
THREAD_PANEL_COLUMN_CLASS,
THREAD_PANEL_COMPOSER_GUTTER_CLASS,
THREAD_PANEL_MESSAGE_GUTTER_CLASS,
@@ -256,9 +255,6 @@ export function MessageThreadPanel({
!isHuddleTranscript && (isOverlay || isSinglePanelView || isFocusMode),
);
const hasConstrainedColumn = columnMaxWidthPx != null;
const messageGutterClass = hasConstrainedColumn
? THREAD_FOCUS_MESSAGE_GUTTER_CLASS
: THREAD_PANEL_MESSAGE_GUTTER_CLASS;
// Whether the composer dock trades its quiet-state spacer for the
// conditional activity accessory (agent working and/or someone typing).
const hasComposerBottomActivity =
@@ -573,18 +569,19 @@ export function MessageThreadPanel({
}
>
{isHuddleTranscript ? (
<div className={cn(messageGutterClass, "pb-2 pt-4")}>
<div className={cn(THREAD_PANEL_MESSAGE_GUTTER_CLASS, "pb-2 pt-4")}>
<HuddleTranscriptIntro />
</div>
) : (
<div
className={cn(messageGutterClass, "pb-2 pt-1")}
className={cn(THREAD_PANEL_MESSAGE_GUTTER_CLASS, "pb-2 pt-1")}
data-testid="message-thread-head"
>
<div className="rounded-2xl">
<MessageRow
actionBarPlacement="inside"
channelId={channelId}
currentPubkey={currentPubkey}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}
isFollowingThread={isFollowingThread}
@@ -635,7 +632,7 @@ export function MessageThreadPanel({
{showThreadHeadDivider ? (
<div
className={cn(messageGutterClass, "pb-4 pt-3")}
className={cn(THREAD_PANEL_MESSAGE_GUTTER_CLASS, "pb-4 pt-3")}
data-testid="message-thread-head-divider"
>
<Separator className="bg-border/60" />
@@ -643,7 +640,7 @@ export function MessageThreadPanel({
) : null}
<div
className={cn(messageGutterClass, "pb-4 pt-0")}
className={cn(THREAD_PANEL_MESSAGE_GUTTER_CLASS, "pb-4 pt-0")}
data-testid="message-thread-replies"
>
{threadRepliesPending && !isHuddleTranscript ? (
@@ -717,6 +714,7 @@ export function MessageThreadPanel({
{showUnreadDivider ? <UnreadDivider /> : null}
<MessageRow
channelId={channelId}
currentPubkey={currentPubkey}
collapseDepthGuideActions={collapseDepthGuideActions}
collapseDescendantsLabel="Collapse replies"
connectDescendants={

View File

@@ -1,6 +1,5 @@
import type { ThreadPanelLayoutProps } from "@/features/channels/lib/threadPanelLayout";
import {
THREAD_FOCUS_MESSAGE_GUTTER_CLASS,
THREAD_PANEL_COLUMN_CLASS,
THREAD_PANEL_COMPOSER_GUTTER_CLASS,
THREAD_PANEL_MESSAGE_GUTTER_CLASS,
@@ -107,9 +106,6 @@ export function MessageThreadPanelSkeleton({
}: MessageThreadPanelSkeletonProps) {
const isOverlay = useIsThreadPanelOverlay();
const hasConstrainedColumn = columnMaxWidthPx != null;
const messageGutterClass = hasConstrainedColumn
? THREAD_FOCUS_MESSAGE_GUTTER_CLASS
: THREAD_PANEL_MESSAGE_GUTTER_CLASS;
useEscapeKey(onClose, isOverlay || isSinglePanelView || isFocusMode);
const threadHeaderContent = (
@@ -135,12 +131,17 @@ export function MessageThreadPanelSkeleton({
}
>
<div
className={cn(messageGutterClass, "pb-1 pt-0")}
className={cn(THREAD_PANEL_MESSAGE_GUTTER_CLASS, "pb-1 pt-0")}
data-testid="message-thread-head-loading"
>
<ThreadMessageSkeleton isHead />
</div>
<div className={cn("space-y-2.5 pb-3 pt-1", messageGutterClass)}>
<div
className={cn(
"space-y-2.5 pb-3 pt-1",
THREAD_PANEL_MESSAGE_GUTTER_CLASS,
)}
>
<ThreadMessageSkeleton />
<ThreadMessageSkeleton />
<div className="ml-[58px] flex items-center gap-1.5 pt-0.5">

View File

@@ -143,6 +143,7 @@ export function MessageRowItem({
>
<MessageRow
channelId={channelId}
currentPubkey={currentPubkey}
highlighted={false}
hoverBackground={false}
huddleMemberPubkeys={huddleMemberPubkeys}
@@ -202,6 +203,7 @@ export function MessageRowItem({
>
<MessageRow
channelId={channelId}
currentPubkey={currentPubkey}
highlighted={message.id === highlightedMessageId || isSearchActive}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}