mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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(":");
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user