mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Polish thread and media layout (#1239)
This commit is contained in:
@@ -22,7 +22,7 @@ export function HomeLoadingState() {
|
||||
key={row}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-9 w-9 rounded-full" />
|
||||
{row === "first" || row === "third" ? (
|
||||
<Skeleton className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full" />
|
||||
) : null}
|
||||
@@ -71,7 +71,7 @@ export function HomeLoadingState() {
|
||||
<div className="mx-1 mb-3 border-t border-border/60" />
|
||||
) : null}
|
||||
<article className="relative flex items-start gap-2.5">
|
||||
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
|
||||
<Skeleton className="h-9 w-9 shrink-0 rounded-full" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
|
||||
@@ -169,7 +169,7 @@ export function InboxListPane({
|
||||
>
|
||||
<button
|
||||
className={cn(
|
||||
"relative block w-full border-l px-3 py-4 text-left transition-colors after:pointer-events-none after:absolute after:bottom-0 after:left-[3.375rem] after:right-0 after:h-px after:bg-border/45 after:content-['']",
|
||||
"relative block w-full border-l px-3 py-4 text-left transition-colors after:pointer-events-none after:absolute after:bottom-0 after:left-[3.625rem] after:right-0 after:h-px after:bg-border/45 after:content-['']",
|
||||
isSelected
|
||||
? "border-l-transparent bg-[var(--inbox-row-highlight-bg)]"
|
||||
: "border-l-transparent group-hover/inbox-item:bg-[var(--inbox-row-highlight-bg)] group-focus-within/inbox-item:bg-[var(--inbox-row-highlight-bg)] active:bg-muted/40",
|
||||
@@ -182,7 +182,7 @@ export function InboxListPane({
|
||||
<div className="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatarUrl={item.avatarUrl}
|
||||
className="h-8 w-8"
|
||||
className="h-9 w-9"
|
||||
displayName={item.senderLabel}
|
||||
size="md"
|
||||
/>
|
||||
|
||||
@@ -116,7 +116,7 @@ export function InboxMessageRow({
|
||||
<div className="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatarUrl={message.avatarUrl}
|
||||
className="h-8 w-8 shrink-0"
|
||||
className="h-9 w-9 shrink-0"
|
||||
displayName={message.authorLabel}
|
||||
size="md"
|
||||
/>
|
||||
|
||||
@@ -2,48 +2,55 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getThreadReplyAvatarCenterPx,
|
||||
getThreadReplyAvatarCenterYPx,
|
||||
getThreadReplyAvatarCenterRem,
|
||||
getThreadReplyAvatarCenterYRem,
|
||||
getThreadReplyConnectorLayout,
|
||||
getThreadReplyDescendantRailStartYPx,
|
||||
getThreadReplyIndentPx,
|
||||
getThreadReplyDescendantRailStartYRem,
|
||||
getThreadReplyIndentRem,
|
||||
threadReplyLength,
|
||||
} from "./threadTreeLayout.ts";
|
||||
|
||||
test("getThreadReplyIndentPx aligns child avatars to parent text columns", () => {
|
||||
assert.equal(getThreadReplyIndentPx(0), 0);
|
||||
assert.equal(getThreadReplyIndentPx(1), 50);
|
||||
assert.equal(getThreadReplyIndentPx(2), 100);
|
||||
assert.equal(getThreadReplyIndentPx(3), 150);
|
||||
test("getThreadReplyIndentRem uses a visible Tailwind spacing step", () => {
|
||||
assert.equal(getThreadReplyIndentRem(0), 0);
|
||||
assert.equal(getThreadReplyIndentRem(1), 2.25);
|
||||
assert.equal(getThreadReplyIndentRem(2), 4.5);
|
||||
assert.equal(getThreadReplyIndentRem(3), 6.75);
|
||||
});
|
||||
|
||||
test("avatar center helpers expose the rail anchor points", () => {
|
||||
assert.equal(getThreadReplyAvatarCenterPx(0), 32);
|
||||
assert.equal(getThreadReplyAvatarCenterPx(1), 82);
|
||||
assert.equal(getThreadReplyAvatarCenterYPx(), 28);
|
||||
assert.equal(getThreadReplyDescendantRailStartYPx(), 52);
|
||||
assert.equal(getThreadReplyAvatarCenterRem(0), 1.875);
|
||||
assert.equal(getThreadReplyAvatarCenterRem(1), 4.125);
|
||||
assert.equal(getThreadReplyAvatarCenterYRem(), 1.5);
|
||||
assert.equal(getThreadReplyDescendantRailStartYRem(), 2.875);
|
||||
});
|
||||
|
||||
test("getThreadReplyConnectorLayout stops before the child avatar edge", () => {
|
||||
assert.equal(getThreadReplyConnectorLayout(0), null);
|
||||
assert.deepEqual(getThreadReplyConnectorLayout(1), {
|
||||
childOffsetPx: 82,
|
||||
heightPx: 28,
|
||||
parentOffsetPx: 32,
|
||||
widthPx: 26,
|
||||
childOffsetRem: 4.125,
|
||||
heightRem: 1.5,
|
||||
parentOffsetRem: 1.875,
|
||||
widthRem: 0.875,
|
||||
});
|
||||
assert.deepEqual(getThreadReplyConnectorLayout(2), {
|
||||
childOffsetPx: 132,
|
||||
heightPx: 28,
|
||||
parentOffsetPx: 82,
|
||||
widthPx: 26,
|
||||
childOffsetRem: 6.375,
|
||||
heightRem: 1.5,
|
||||
parentOffsetRem: 4.125,
|
||||
widthRem: 0.875,
|
||||
});
|
||||
});
|
||||
|
||||
test("getThreadReplyConnectorLayout clamps very deep replies to the visible rail", () => {
|
||||
assert.deepEqual(getThreadReplyConnectorLayout(99), {
|
||||
childOffsetPx: 332,
|
||||
heightPx: 28,
|
||||
parentOffsetPx: 282,
|
||||
widthPx: 26,
|
||||
childOffsetRem: 15.375,
|
||||
heightRem: 1.5,
|
||||
parentOffsetRem: 13.125,
|
||||
widthRem: 0.875,
|
||||
});
|
||||
});
|
||||
|
||||
test("threadReplyLength formats rem values for inline styles", () => {
|
||||
assert.equal(threadReplyLength(0), "0");
|
||||
assert.equal(threadReplyLength(1.75), "1.75rem");
|
||||
assert.equal(threadReplyLength(-0.125), "-0.125rem");
|
||||
});
|
||||
|
||||
@@ -1,51 +1,60 @@
|
||||
const THREAD_REPLY_MAX_VISIBLE_DEPTH = 6;
|
||||
|
||||
const THREAD_REPLY_AVATAR_SIZE_PX = 40;
|
||||
const THREAD_REPLY_ROW_CONTENT_INSET_PX = 12;
|
||||
const THREAD_REPLY_ROW_CONTENT_GAP_PX = 10;
|
||||
const THREAD_REPLY_ROW_PADDING_TOP_PX = 8;
|
||||
const THREAD_REPLY_AVATAR_RADIUS_PX = THREAD_REPLY_AVATAR_SIZE_PX / 2;
|
||||
const THREAD_REPLY_AVATAR_LINE_GAP_PX = 4;
|
||||
const THREAD_REPLY_AVATAR_SIZE_REM = 2.25; // Tailwind size-9
|
||||
const THREAD_REPLY_ROW_MARGIN_INLINE_REM = 0.25; // Tailwind mx-1
|
||||
const THREAD_REPLY_ROW_CONTENT_INSET_REM = 0.5; // Tailwind px-2
|
||||
const THREAD_REPLY_ROW_CONTENT_GAP_REM = 0.625; // Tailwind gap-2.5
|
||||
const THREAD_REPLY_ROW_PADDING_TOP_REM = 0.375; // Tailwind py-1.5
|
||||
const THREAD_REPLY_DEPTH_STEP_REM = 2.25; // Tailwind spacing-9
|
||||
const THREAD_REPLY_AVATAR_RADIUS_REM = THREAD_REPLY_AVATAR_SIZE_REM / 2;
|
||||
const THREAD_REPLY_AVATAR_LINE_GAP_REM = 0.25; // Tailwind spacing-1
|
||||
|
||||
export const THREAD_REPLY_BODY_OFFSET_PX =
|
||||
THREAD_REPLY_ROW_CONTENT_INSET_PX +
|
||||
THREAD_REPLY_AVATAR_SIZE_PX +
|
||||
THREAD_REPLY_ROW_CONTENT_GAP_PX;
|
||||
export const THREAD_REPLY_ROOT_INDENT_PX =
|
||||
THREAD_REPLY_BODY_OFFSET_PX - THREAD_REPLY_ROW_CONTENT_INSET_PX;
|
||||
export const THREAD_REPLY_NESTED_INDENT_PX = THREAD_REPLY_ROOT_INDENT_PX;
|
||||
export const THREAD_REPLY_LINE_WIDTH_PX = 1.5;
|
||||
export const THREAD_REPLY_BODY_OFFSET_REM =
|
||||
THREAD_REPLY_ROW_MARGIN_INLINE_REM +
|
||||
THREAD_REPLY_ROW_CONTENT_INSET_REM +
|
||||
THREAD_REPLY_AVATAR_SIZE_REM +
|
||||
THREAD_REPLY_ROW_CONTENT_GAP_REM;
|
||||
export const THREAD_REPLY_ROOT_INDENT_REM = THREAD_REPLY_DEPTH_STEP_REM;
|
||||
export const THREAD_REPLY_NESTED_INDENT_REM = THREAD_REPLY_ROOT_INDENT_REM;
|
||||
export const THREAD_REPLY_LINE_WIDTH_REM = 0.09375;
|
||||
|
||||
const THREAD_REPLY_AVATAR_CENTER_OFFSET_PX =
|
||||
THREAD_REPLY_ROW_CONTENT_INSET_PX + THREAD_REPLY_AVATAR_SIZE_PX / 2;
|
||||
const THREAD_REPLY_AVATAR_CENTER_Y_PX =
|
||||
THREAD_REPLY_ROW_PADDING_TOP_PX + THREAD_REPLY_AVATAR_SIZE_PX / 2;
|
||||
const THREAD_REPLY_AVATAR_CENTER_OFFSET_REM =
|
||||
THREAD_REPLY_ROW_MARGIN_INLINE_REM +
|
||||
THREAD_REPLY_ROW_CONTENT_INSET_REM +
|
||||
THREAD_REPLY_AVATAR_SIZE_REM / 2;
|
||||
const THREAD_REPLY_AVATAR_CENTER_Y_REM =
|
||||
THREAD_REPLY_ROW_PADDING_TOP_REM + THREAD_REPLY_AVATAR_SIZE_REM / 2;
|
||||
|
||||
export function threadReplyLength(valueRem: number) {
|
||||
if (valueRem === 0) return "0";
|
||||
return `${Number(valueRem.toFixed(5))}rem`;
|
||||
}
|
||||
|
||||
function clampVisibleDepth(depth: number) {
|
||||
return Math.min(Math.max(depth, 0), THREAD_REPLY_MAX_VISIBLE_DEPTH);
|
||||
}
|
||||
|
||||
export function getThreadReplyIndentPx(depth: number) {
|
||||
export function getThreadReplyIndentRem(depth: number) {
|
||||
const visibleDepth = clampVisibleDepth(depth);
|
||||
return visibleDepth > 0
|
||||
? THREAD_REPLY_ROOT_INDENT_PX +
|
||||
(visibleDepth - 1) * THREAD_REPLY_NESTED_INDENT_PX
|
||||
? THREAD_REPLY_ROOT_INDENT_REM +
|
||||
(visibleDepth - 1) * THREAD_REPLY_NESTED_INDENT_REM
|
||||
: 0;
|
||||
}
|
||||
|
||||
export function getThreadReplyAvatarCenterPx(depth: number) {
|
||||
return getThreadReplyIndentPx(depth) + THREAD_REPLY_AVATAR_CENTER_OFFSET_PX;
|
||||
export function getThreadReplyAvatarCenterRem(depth: number) {
|
||||
return getThreadReplyIndentRem(depth) + THREAD_REPLY_AVATAR_CENTER_OFFSET_REM;
|
||||
}
|
||||
|
||||
export function getThreadReplyAvatarCenterYPx() {
|
||||
return THREAD_REPLY_AVATAR_CENTER_Y_PX;
|
||||
export function getThreadReplyAvatarCenterYRem() {
|
||||
return THREAD_REPLY_AVATAR_CENTER_Y_REM;
|
||||
}
|
||||
|
||||
export function getThreadReplyDescendantRailStartYPx() {
|
||||
export function getThreadReplyDescendantRailStartYRem() {
|
||||
return (
|
||||
THREAD_REPLY_AVATAR_CENTER_Y_PX +
|
||||
THREAD_REPLY_AVATAR_RADIUS_PX +
|
||||
THREAD_REPLY_AVATAR_LINE_GAP_PX
|
||||
THREAD_REPLY_AVATAR_CENTER_Y_REM +
|
||||
THREAD_REPLY_AVATAR_RADIUS_REM +
|
||||
THREAD_REPLY_AVATAR_LINE_GAP_REM
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,17 +64,17 @@ export function getThreadReplyConnectorLayout(depth: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentOffsetPx = getThreadReplyAvatarCenterPx(visibleDepth - 1);
|
||||
const childOffsetPx = getThreadReplyAvatarCenterPx(visibleDepth);
|
||||
const childEdgeOffsetPx =
|
||||
childOffsetPx -
|
||||
THREAD_REPLY_AVATAR_RADIUS_PX -
|
||||
THREAD_REPLY_AVATAR_LINE_GAP_PX;
|
||||
const parentOffsetRem = getThreadReplyAvatarCenterRem(visibleDepth - 1);
|
||||
const childOffsetRem = getThreadReplyAvatarCenterRem(visibleDepth);
|
||||
const childEdgeOffsetRem =
|
||||
childOffsetRem -
|
||||
THREAD_REPLY_AVATAR_RADIUS_REM -
|
||||
THREAD_REPLY_AVATAR_LINE_GAP_REM;
|
||||
|
||||
return {
|
||||
childOffsetPx,
|
||||
heightPx: THREAD_REPLY_AVATAR_CENTER_Y_PX,
|
||||
parentOffsetPx,
|
||||
widthPx: Math.max(0, childEdgeOffsetPx - parentOffsetPx),
|
||||
childOffsetRem,
|
||||
heightRem: THREAD_REPLY_AVATAR_CENTER_Y_REM,
|
||||
parentOffsetRem,
|
||||
widthRem: Math.max(0, childEdgeOffsetRem - parentOffsetRem),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
import { useRemindLater } from "@/features/reminders/ui/RemindMeLaterProvider";
|
||||
import {
|
||||
getThreadReplyAvatarCenterPx,
|
||||
getThreadReplyAvatarCenterYPx,
|
||||
getThreadReplyDescendantRailStartYPx,
|
||||
getThreadReplyAvatarCenterRem,
|
||||
getThreadReplyAvatarCenterYRem,
|
||||
getThreadReplyDescendantRailStartYRem,
|
||||
getThreadReplyConnectorLayout,
|
||||
getThreadReplyIndentPx,
|
||||
THREAD_REPLY_LINE_WIDTH_PX,
|
||||
getThreadReplyIndentRem,
|
||||
threadReplyLength,
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
} from "@/features/messages/lib/threadTreeLayout";
|
||||
import { KIND_STREAM_MESSAGE_DIFF } from "@/shared/constants/kinds";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -186,9 +187,9 @@ export const MessageRow = React.memo(
|
||||
[channels],
|
||||
);
|
||||
|
||||
const indentPx = getThreadReplyIndentPx(message.depth);
|
||||
const descendantGuideOffsetPx = connectDescendants
|
||||
? getThreadReplyAvatarCenterPx(message.depth)
|
||||
const indentRem = getThreadReplyIndentRem(message.depth);
|
||||
const descendantGuideOffsetRem = connectDescendants
|
||||
? getThreadReplyAvatarCenterRem(message.depth)
|
||||
: null;
|
||||
const replyConnector = React.useMemo(() => {
|
||||
return getThreadReplyConnectorLayout(message.depth);
|
||||
@@ -200,7 +201,7 @@ export const MessageRow = React.memo(
|
||||
|
||||
return depths.map((depth) => ({
|
||||
depth,
|
||||
offset: getThreadReplyAvatarCenterPx(depth),
|
||||
offset: getThreadReplyAvatarCenterRem(depth),
|
||||
}));
|
||||
}, [depthGuideDepths, message.depth]);
|
||||
const handleCollapseDescendants = React.useCallback(
|
||||
@@ -293,8 +294,7 @@ export const MessageRow = React.memo(
|
||||
};
|
||||
|
||||
const isThreadReplyLayout = layoutVariant === "thread-reply";
|
||||
const guideBleedPx = isThreadReplyLayout ? 4 : 0;
|
||||
const avatarSizeClass = "!h-10 !w-10";
|
||||
const guideBleedRem = isThreadReplyLayout ? 0.25 : 0;
|
||||
const avatarButtonRadiusClass = "rounded-full";
|
||||
|
||||
const respondToDotColor =
|
||||
@@ -309,7 +309,7 @@ export const MessageRow = React.memo(
|
||||
<UserAvatar
|
||||
accent={message.accent}
|
||||
avatarUrl={message.avatarUrl ?? null}
|
||||
className={cn("shrink-0", avatarSizeClass)}
|
||||
className="shrink-0"
|
||||
displayName={message.author}
|
||||
testId="message-avatar"
|
||||
/>
|
||||
@@ -442,7 +442,11 @@ export const MessageRow = React.memo(
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={indentPx > 0 ? { paddingLeft: `${indentPx}px` } : undefined}
|
||||
style={
|
||||
indentRem > 0
|
||||
? { paddingLeft: threadReplyLength(indentRem) }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{showDepthGuides && depthGuideItems.length > 0 ? (
|
||||
<div
|
||||
@@ -455,8 +459,8 @@ export const MessageRow = React.memo(
|
||||
"pointer-events-none",
|
||||
)}
|
||||
style={{
|
||||
bottom: `${-guideBleedPx}px`,
|
||||
top: `${-guideBleedPx}px`,
|
||||
bottom: threadReplyLength(-guideBleedRem),
|
||||
top: threadReplyLength(-guideBleedRem),
|
||||
}}
|
||||
>
|
||||
{depthGuideItems.map(({ depth, offset }) => {
|
||||
@@ -472,11 +476,13 @@ export const MessageRow = React.memo(
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
|
||||
isHighlighted ? "border-primary" : "border-border",
|
||||
isHighlighted ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
style={{
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
left: `${offset}px`,
|
||||
borderLeftWidth: threadReplyLength(
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
),
|
||||
left: threadReplyLength(offset),
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -511,7 +517,7 @@ export const MessageRow = React.memo(
|
||||
false,
|
||||
)
|
||||
}
|
||||
style={{ left: `${offset}px` }}
|
||||
style={{ left: threadReplyLength(offset) }}
|
||||
type="button"
|
||||
/>
|
||||
</React.Fragment>
|
||||
@@ -523,31 +529,33 @@ export const MessageRow = React.memo(
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
|
||||
isHighlighted ? "border-primary" : "border-border",
|
||||
isHighlighted ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
key={`${message.id}-depth-guide-${offset}`}
|
||||
style={{
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
left: `${offset}px`,
|
||||
borderLeftWidth: threadReplyLength(
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
),
|
||||
left: threadReplyLength(offset),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{showDepthGuides && descendantGuideOffsetPx !== null ? (
|
||||
{showDepthGuides && descendantGuideOffsetRem !== null ? (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 z-0 border-l transition-[border-color]",
|
||||
highlightDescendantRail ? "border-primary" : "border-border",
|
||||
highlightDescendantRail ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
style={{
|
||||
bottom: `${-guideBleedPx}px`,
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
left: `${descendantGuideOffsetPx}px`,
|
||||
top: `${getThreadReplyDescendantRailStartYPx()}px`,
|
||||
bottom: threadReplyLength(-guideBleedRem),
|
||||
borderLeftWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
|
||||
left: threadReplyLength(descendantGuideOffsetRem),
|
||||
top: threadReplyLength(getThreadReplyDescendantRailStartYRem()),
|
||||
}}
|
||||
/>
|
||||
{onCollapseDescendants ? (
|
||||
@@ -564,8 +572,8 @@ export const MessageRow = React.memo(
|
||||
onMouseEnter={() => handleCollapseDescendantsHoverChange(true)}
|
||||
onMouseLeave={() => handleCollapseDescendantsHoverChange(false)}
|
||||
style={{
|
||||
left: `${descendantGuideOffsetPx}px`,
|
||||
top: `${getThreadReplyAvatarCenterYPx()}px`,
|
||||
left: threadReplyLength(descendantGuideOffsetRem),
|
||||
top: threadReplyLength(getThreadReplyAvatarCenterYRem()),
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
@@ -577,15 +585,17 @@ export const MessageRow = React.memo(
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute left-0 top-0 rounded-bl-2xl border-b border-l transition-[border-color]",
|
||||
highlightReplyConnector ? "border-primary" : "border-border",
|
||||
highlightReplyConnector ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
style={{
|
||||
borderBottomWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
height: `${replyConnector.heightPx + guideBleedPx}px`,
|
||||
left: `${replyConnector.parentOffsetPx}px`,
|
||||
top: `${-guideBleedPx}px`,
|
||||
width: `${replyConnector.widthPx}px`,
|
||||
borderBottomWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
|
||||
borderLeftWidth: threadReplyLength(THREAD_REPLY_LINE_WIDTH_REM),
|
||||
height: threadReplyLength(
|
||||
replyConnector.heightRem + guideBleedRem,
|
||||
),
|
||||
left: threadReplyLength(replyConnector.parentOffsetRem),
|
||||
top: threadReplyLength(-guideBleedRem),
|
||||
width: threadReplyLength(replyConnector.widthRem),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -94,12 +94,10 @@ type MessageThreadPanelProps = {
|
||||
onUnfollowThread?: () => void;
|
||||
};
|
||||
|
||||
/** Stable `useDeferredValue` initial value; mirrors `EMPTY_MESSAGES`. */
|
||||
const EMPTY_THREAD_REPLIES: MainTimelineEntry[] = [];
|
||||
const THREAD_PANEL_MESSAGE_GUTTER_CLASS = "px-2";
|
||||
const THREAD_PANEL_COMPOSER_GUTTER_CLASS = "px-5";
|
||||
const THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX = -2;
|
||||
|
||||
const THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM = -0.125;
|
||||
type MessageThreadPanelSkeletonProps = {
|
||||
isSinglePanelView?: boolean;
|
||||
layout?: "standalone" | "split";
|
||||
@@ -696,7 +694,9 @@ export function MessageThreadPanel({
|
||||
message={threadHead}
|
||||
onOpenThread={expandThreadHeadReplies}
|
||||
summary={visibleThreadHeadSummary}
|
||||
summaryIndentOffsetPx={THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX}
|
||||
summaryIndentOffsetRem={
|
||||
THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM
|
||||
}
|
||||
unreadCount={threadUnreadCount}
|
||||
/>
|
||||
</div>
|
||||
@@ -820,8 +820,8 @@ export function MessageThreadPanel({
|
||||
}
|
||||
onOpenThread={onExpandReplies}
|
||||
summary={entry.summary}
|
||||
summaryIndentOffsetPx={
|
||||
THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX
|
||||
summaryIndentOffsetRem={
|
||||
THREAD_PANEL_SUMMARY_INDENT_OFFSET_REM
|
||||
}
|
||||
showDepthGuides={shouldShowThreadBranchGuides}
|
||||
unreadCount={threadReplyUnreadCounts?.get(
|
||||
|
||||
@@ -8,10 +8,11 @@ import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { ThreadDepthGuideAction } from "@/features/messages/ui/MessageRow";
|
||||
import { formatThreadSummaryLastReplyTime } from "@/features/messages/lib/dateFormatters";
|
||||
import {
|
||||
getThreadReplyAvatarCenterPx,
|
||||
getThreadReplyIndentPx,
|
||||
THREAD_REPLY_BODY_OFFSET_PX,
|
||||
THREAD_REPLY_LINE_WIDTH_PX,
|
||||
getThreadReplyAvatarCenterRem,
|
||||
getThreadReplyIndentRem,
|
||||
threadReplyLength,
|
||||
THREAD_REPLY_BODY_OFFSET_REM,
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
} from "@/features/messages/lib/threadTreeLayout";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
@@ -59,7 +60,7 @@ export function MessageThreadSummaryRow({
|
||||
onOpenThread,
|
||||
showDepthGuides = true,
|
||||
summary,
|
||||
summaryIndentOffsetPx = 0,
|
||||
summaryIndentOffsetRem = 0,
|
||||
unreadCount,
|
||||
}: {
|
||||
collapseDepthGuideActions?: ReadonlyArray<ThreadDepthGuideAction>;
|
||||
@@ -75,12 +76,12 @@ export function MessageThreadSummaryRow({
|
||||
onOpenThread: (message: TimelineMessage) => void;
|
||||
showDepthGuides?: boolean;
|
||||
summary: TimelineThreadSummary;
|
||||
summaryIndentOffsetPx?: number;
|
||||
summaryIndentOffsetRem?: number;
|
||||
unreadCount?: number;
|
||||
}) {
|
||||
const indentPx = getThreadReplyIndentPx(depth);
|
||||
const marginLeftPx =
|
||||
indentPx + THREAD_REPLY_BODY_OFFSET_PX + summaryIndentOffsetPx;
|
||||
const indentRem = getThreadReplyIndentRem(depth);
|
||||
const marginLeftRem =
|
||||
indentRem + THREAD_REPLY_BODY_OFFSET_REM + summaryIndentOffsetRem;
|
||||
const replyLabel = summary.replyCount === 1 ? "reply" : "replies";
|
||||
const summaryAriaLabel = summary.lastReplyAt
|
||||
? `View thread with ${summary.replyCount} ${replyLabel}, last reply ${formatThreadSummaryLastReplyTime(summary.lastReplyAt)}`
|
||||
@@ -90,7 +91,7 @@ export function MessageThreadSummaryRow({
|
||||
: Array.from({ length: depth }, (_, index) => index);
|
||||
const depthGuideItems = guideDepths.map((guideDepth) => ({
|
||||
depth: guideDepth,
|
||||
offset: getThreadReplyAvatarCenterPx(guideDepth),
|
||||
offset: getThreadReplyAvatarCenterRem(guideDepth),
|
||||
}));
|
||||
const collapseDepthGuideActionsByDepth = new Map(
|
||||
collapseDepthGuideActions?.map((action) => [action.depth, action]) ?? [],
|
||||
@@ -108,7 +109,7 @@ export function MessageThreadSummaryRow({
|
||||
collapseDepthGuideActionsByDepth.size === 0 &&
|
||||
"pointer-events-none",
|
||||
)}
|
||||
style={{ bottom: "-4px", top: "-4px" }}
|
||||
style={{ bottom: "-0.25rem", top: "-0.25rem" }}
|
||||
>
|
||||
{depthGuideItems.map(({ depth: guideDepth, offset }) => {
|
||||
const collapseAction =
|
||||
@@ -125,11 +126,13 @@ export function MessageThreadSummaryRow({
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
|
||||
isHighlighted ? "border-primary" : "border-border",
|
||||
isHighlighted ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
style={{
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
left: `${offset}px`,
|
||||
borderLeftWidth: threadReplyLength(
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
),
|
||||
left: threadReplyLength(offset),
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -166,7 +169,7 @@ export function MessageThreadSummaryRow({
|
||||
false,
|
||||
)
|
||||
}
|
||||
style={{ left: `${offset}px` }}
|
||||
style={{ left: threadReplyLength(offset) }}
|
||||
type="button"
|
||||
/>
|
||||
</React.Fragment>
|
||||
@@ -178,12 +181,14 @@ export function MessageThreadSummaryRow({
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-0 top-0 border-l transition-[border-color]",
|
||||
isHighlighted ? "border-primary" : "border-border",
|
||||
isHighlighted ? "border-primary" : "border-border/45",
|
||||
)}
|
||||
key={`${message.id}-summary-depth-guide-${offset}`}
|
||||
style={{
|
||||
borderLeftWidth: `${THREAD_REPLY_LINE_WIDTH_PX}px`,
|
||||
left: `${offset}px`,
|
||||
borderLeftWidth: threadReplyLength(
|
||||
THREAD_REPLY_LINE_WIDTH_REM,
|
||||
),
|
||||
left: threadReplyLength(offset),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -197,7 +202,7 @@ export function MessageThreadSummaryRow({
|
||||
data-thread-head-id={message.id}
|
||||
data-testid="message-thread-summary"
|
||||
onClick={() => onOpenThread(message)}
|
||||
style={{ marginLeft: `${marginLeftPx}px` }}
|
||||
style={{ marginLeft: threadReplyLength(marginLeftRem) }}
|
||||
type="button"
|
||||
>
|
||||
<div className="ml-0.5 flex shrink-0 items-center">
|
||||
|
||||
@@ -1443,7 +1443,8 @@
|
||||
}
|
||||
|
||||
.buzz-spoiler--block img[data-spoiler-media-size] {
|
||||
height: var(--buzz-spoiler-media-height);
|
||||
aspect-ratio: var(--buzz-spoiler-media-aspect-ratio);
|
||||
height: auto;
|
||||
width: var(--buzz-spoiler-media-width);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ type UserAvatarSize = "xs" | "sm" | "md";
|
||||
const sizeClasses: Record<UserAvatarSize, string> = {
|
||||
xs: "h-5 w-5 text-3xs",
|
||||
sm: "h-6 w-6 text-2xs",
|
||||
md: "h-10 w-10 text-xs",
|
||||
md: "h-9 w-9 text-xs",
|
||||
};
|
||||
|
||||
type UserAvatarProps = {
|
||||
|
||||
@@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
"relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1118,9 +1118,10 @@ function ImageBlock({
|
||||
|
||||
const spoilerMediaStyle = hiddenSpoilerMediaSize
|
||||
? ({
|
||||
"--buzz-spoiler-media-height": `${hiddenSpoilerMediaSize.height}px`,
|
||||
"--buzz-spoiler-media-aspect-ratio": `${hiddenSpoilerMediaSize.width} / ${hiddenSpoilerMediaSize.height}`,
|
||||
"--buzz-spoiler-media-width": `${hiddenSpoilerMediaSize.width}px`,
|
||||
height: `${hiddenSpoilerMediaSize.height}px`,
|
||||
aspectRatio: `${hiddenSpoilerMediaSize.width} / ${hiddenSpoilerMediaSize.height}`,
|
||||
height: "auto",
|
||||
width: `${hiddenSpoilerMediaSize.width}px`,
|
||||
} as React.CSSProperties)
|
||||
: undefined;
|
||||
@@ -1202,7 +1203,7 @@ function ImageBlock({
|
||||
aria-hidden={isHiddenInSpoiler ? true : undefined}
|
||||
aria-label={alt?.trim() ? `Zoom image: ${alt}` : "Zoom image"}
|
||||
className={cn(
|
||||
"mt-1 inline-block max-w-full cursor-zoom-in rounded-xl border-0 bg-transparent p-0 text-left align-top focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
|
||||
"mt-1 inline-block min-w-0 max-w-full cursor-zoom-in rounded-xl border-0 bg-transparent p-0 text-left align-top focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
|
||||
lightboxBox && "opacity-0",
|
||||
)}
|
||||
data-testid="message-image-lightbox-trigger"
|
||||
@@ -1214,7 +1215,7 @@ function ImageBlock({
|
||||
>
|
||||
<img
|
||||
alt={alt}
|
||||
className="block max-h-64 max-w-sm rounded-xl object-contain"
|
||||
className="block h-auto max-h-64 max-w-[min(24rem,100%)] rounded-xl object-contain"
|
||||
data-spoiler-media-size={hiddenSpoilerMediaSize ? "" : undefined}
|
||||
height={intrinsicDimensions?.height}
|
||||
ref={imageRef}
|
||||
@@ -1981,7 +1982,7 @@ function createMarkdownComponents(
|
||||
}
|
||||
const entry = src ? imetaByUrl?.get(src) : undefined;
|
||||
return (
|
||||
<span data-block-media="" className="block">
|
||||
<span data-block-media="" className="block min-w-0 max-w-full">
|
||||
<ImageBlock
|
||||
alt={alt}
|
||||
dim={entry?.dim}
|
||||
@@ -2005,7 +2006,7 @@ function createMarkdownComponents(
|
||||
|
||||
if (isImageOnlyParagraph(childArray)) {
|
||||
return (
|
||||
<div className="mt-1 grid max-w-lg grid-cols-2 gap-1.5 [&_br]:hidden [&_[data-block-media]]:mt-0 [&_[data-block-media]]:max-w-none [&_img]:mt-0 [&_img]:w-full [&_img]:max-w-full">
|
||||
<div className="mt-1 grid w-full min-w-0 max-w-lg grid-cols-2 gap-1.5 [&_br]:hidden [&_[data-block-media]]:mt-0 [&_[data-block-media]]:max-w-none [&_img]:mt-0 [&_img]:w-full [&_img]:max-w-full">
|
||||
{imageChildren}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user