refactor(desktop): satisfy prototype quality gates

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
Arjun Mahanti
2026-08-26 13:56:42 -04:00
parent 84d23270b9
commit 86d9bfff8d
7 changed files with 89 additions and 90 deletions

View File

@@ -207,7 +207,7 @@ export function BestieChatPopover() {
>
<ProfileAvatar
avatarUrl={bestie.avatarUrl}
className="size-6 text-[9px]"
className="size-6 text-3xs"
label={bestie.name}
plain
testId="bestie-header-avatar"

View File

@@ -110,10 +110,10 @@ export function BestieMessagePanel({
/>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1">
<p className="truncate text-[10px] font-semibold leading-3.5">
<p className="truncate text-2xs font-semibold leading-3.5">
{message.author}
</p>
<p className="shrink-0 text-[9px] leading-3.5 text-muted-foreground/70">
<p className="shrink-0 text-3xs leading-3.5 text-muted-foreground/70">
{message.time}
</p>
</div>
@@ -122,7 +122,7 @@ export function BestieMessagePanel({
data-testid={`bestie-message-snapshot-body-${message.id}`}
>
<VideoReviewCommentMarkdown
className="line-clamp-1 text-[10px] leading-3.5 text-foreground/80 [&_p]:leading-3.5"
className="line-clamp-1 text-2xs leading-3.5 text-foreground/80 [&_p]:leading-3.5"
content={message.body}
interactive={false}
messageId={message.id}

View File

@@ -17,10 +17,8 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import * as React from "react";
import { toast } from "sonner";
import { buildMessageLink } from "@/features/messages/lib/messageLink";
import { EmojiPicker } from "@/features/custom-emoji/ui/EmojiPicker";
import { useCustomEmoji } from "@/features/custom-emoji/hooks";
import { getThreadReference } from "@/features/messages/lib/threading";
import { ReportMessageDialog } from "@/features/moderation/ui/ReportMessageDialog";
import { MessageModerationInlineItems } from "@/features/moderation/ui/MessageModerationMenuItems";
import type {
@@ -34,8 +32,6 @@ import {
import { reactionEmojiUrl } from "@/shared/api/customEmoji";
import { cn } from "@/shared/lib/cn";
import { copyTextToClipboard } from "@/shared/lib/clipboard";
import { emojiDisplayName } from "@/shared/lib/emojiName";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { KIND_HUDDLE_STARTED } from "@/shared/constants/kinds";
import { Button } from "@/shared/ui/button";
import { HashArrowIn } from "@/shared/ui/icons";
@@ -53,36 +49,16 @@ import {
MESSAGE_ACTION_BLOOM_VISUAL_DURATION,
MessageActionBloomSurface,
} from "./MessageActionBloomSurface";
import {
canCopyMessageLink,
copyMessageLink,
isCustomEmojiShortcode,
QuickReactionButton,
} from "./MessageActionToolbarHelpers";
const ACTION_BUTTON_CLASS = "h-8 w-8 rounded-full p-0";
const ACTION_ICON_CLASS = "!h-4 !w-4";
/** Copying a message link is offered from both the hover action bar and the
* More menu; both paths share this exact link-building + toast behavior. */
function copyMessageLink(channelId: string, message: TimelineMessage) {
const { rootId } = getThreadReference(message.tags ?? []);
const link = buildMessageLink({
channelId,
messageId: message.id,
threadRootId: rootId,
});
copyTextToClipboard(link, "Link copied to clipboard");
}
/** Gate shared by every copy-link surface: pending sends have no delivered
* event to link to, huddle system rows aren't linkable, and callers without
* a channelId (e.g. inbox preview rows) can't build the link. */
function canCopyMessageLink(
message: TimelineMessage,
channelId: string | null | undefined,
): channelId is string {
return (
!message.pending &&
message.kind !== KIND_HUDDLE_STARTED &&
Boolean(channelId)
);
}
function MoreActionsPanel({
channelId,
message,
@@ -374,51 +350,6 @@ function MoreActionsPanel({
);
}
function QuickReactionButton({
customEmojiUrl,
emoji,
onSelect,
}: {
customEmojiUrl?: string;
emoji: string;
onSelect: (emoji: string) => void;
}) {
const displayName = emojiDisplayName(emoji);
const mediaUrl = customEmojiUrl ? rewriteRelayUrl(customEmojiUrl) : null;
return (
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label={`React with ${displayName}`}
className="flex h-8 w-8 items-center justify-center rounded-full text-base leading-none text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
onClick={() => onSelect(emoji)}
title={displayName}
type="button"
>
{mediaUrl ? (
<img
alt={emoji}
className="h-5 w-5 object-contain"
draggable={false}
src={mediaUrl}
/>
) : (
<span aria-hidden="true" className="translate-y-px">
{emoji}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent>{displayName}</TooltipContent>
</Tooltip>
);
}
function isCustomEmojiShortcode(emoji: string) {
return emoji.startsWith(":") && emoji.endsWith(":");
}
export const MessageActionBar = React.memo(function MessageActionBar({
channelId,
message,
@@ -834,7 +765,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
>
<ProfileAvatar
avatarUrl={bestie.avatarUrl}
className="size-4 text-[7px] shadow-none"
className="size-4 text-3xs shadow-none"
label={bestie.name}
plain
testId={`bestie-action-avatar-${message.id}`}

View File

@@ -0,0 +1,74 @@
import { buildMessageLink } from "@/features/messages/lib/messageLink";
import { getThreadReference } from "@/features/messages/lib/threading";
import type { TimelineMessage } from "@/features/messages/types";
import { KIND_HUDDLE_STARTED } from "@/shared/constants/kinds";
import { copyTextToClipboard } from "@/shared/lib/clipboard";
import { emojiDisplayName } from "@/shared/lib/emojiName";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
export function copyMessageLink(channelId: string, message: TimelineMessage) {
const { rootId } = getThreadReference(message.tags ?? []);
const link = buildMessageLink({
channelId,
messageId: message.id,
threadRootId: rootId,
});
copyTextToClipboard(link, "Link copied to clipboard");
}
export function canCopyMessageLink(
message: TimelineMessage,
channelId: string | null | undefined,
): channelId is string {
return (
!message.pending &&
message.kind !== KIND_HUDDLE_STARTED &&
Boolean(channelId)
);
}
export function QuickReactionButton({
customEmojiUrl,
emoji,
onSelect,
}: {
customEmojiUrl?: string;
emoji: string;
onSelect: (emoji: string) => void;
}) {
const displayName = emojiDisplayName(emoji);
const mediaUrl = customEmojiUrl ? rewriteRelayUrl(customEmojiUrl) : null;
return (
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label={`React with ${displayName}`}
className="flex h-8 w-8 items-center justify-center rounded-full text-base leading-none text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
onClick={() => onSelect(emoji)}
title={displayName}
type="button"
>
{mediaUrl ? (
<img
alt={emoji}
className="h-5 w-5 object-contain"
draggable={false}
src={mediaUrl}
/>
) : (
<span aria-hidden="true" className="translate-y-px">
{emoji}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent>{displayName}</TooltipContent>
</Tooltip>
);
}
export function isCustomEmojiShortcode(emoji: string) {
return emoji.startsWith(":") && emoji.endsWith(":");
}

View File

@@ -419,16 +419,13 @@ function MessageComposerImpl({
setSpoileredAttachmentUrls(restoredSpoileredAttachmentUrls);
}
}, [editTarget?.id]);
// ── Focus on reply ──────────────────────────────────────────────────
// Use focusPreserve so that re-renders (e.g. new messages arriving in
// a thread) don't yank the cursor to the end while the user is editing.
// Preserve the cursor when reply-driven rerenders refocus the composer.
React.useEffect(() => {
if (!replyTarget || composerDisabled) return;
richText.focusPreserve();
}, [composerDisabled, replyTarget, richText.focusPreserve]);
useComposerAutofocus(richText.focus, effectiveDraftKey, composerDisabled);
// Hooks return a plain-text edit descriptor; `replacePlainTextRange`
// applies it as a single ProseMirror transaction (no markdown round-trip).
// Apply autocomplete as one ProseMirror transaction, without a markdown round-trip.
const applyAutocompleteEdit = React.useCallback(
(edit: AutocompleteEdit) => {
richText.replacePlainTextRange(

View File

@@ -933,9 +933,8 @@ export const MessageRow = React.memo(
</article>
</div>
);
// Callbacks (onReply, onToggleReaction) intentionally excluded: inline arrows
// from parent create new refs every render — including them defeats memo.
},
// Ignore unstable callback identities; compare createdAt for timestamp-derived UI.
(prev, next) =>
prev.message.id === next.message.id &&
prev.message.pubkey === next.message.pubkey &&
@@ -946,8 +945,6 @@ export const MessageRow = React.memo(
prev.message.ownerLabel === next.message.ownerLabel &&
prev.message.avatarUrl === next.message.avatarUrl &&
prev.message.accent === next.message.accent &&
// The header timestamp and hover gutter both derive from createdAt (the
// old `time` prop was the same value pre-formatted; this row reads neither).
prev.message.createdAt === next.message.createdAt &&
prev.message.depth === next.message.depth &&
prev.message.kind === next.message.kind &&

View File

@@ -182,7 +182,7 @@ export function AppSidebarPrimaryMenu({
>
<ProfileAvatar
avatarUrl={bestieAgent.avatarUrl}
className="size-4 text-[7px] shadow-none"
className="size-4 text-3xs shadow-none"
label={bestieAgent.name}
plain
testId="bestie-sidebar-avatar"