From fccf0d946e7f95f7e0de9d8acb16024cb31254df Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Tue, 2 Jun 2026 00:12:47 -0700 Subject: [PATCH 01/15] refactor: simplify message and push presentation --- apps/web/app/(app)/post/new/CopyCommand.tsx | 44 +-- apps/web/components/app/feed/FeedList.tsx | 12 +- .../app/messages/MessagePresentation.tsx | 227 +++++++++++++++ .../components/app/messages/MessagesInbox.tsx | 274 ++---------------- .../components/app/messages/message-utils.ts | 46 +++ .../components/app/profile/InviteButton.tsx | 13 +- apps/web/components/landing/Hero.tsx | 29 +- apps/web/lib/utils/useClipboardFeedback.ts | 34 +++ packages/cli/src/commands/push-output.ts | 67 +++++ packages/cli/src/commands/push.ts | 51 +--- 10 files changed, 438 insertions(+), 359 deletions(-) create mode 100644 apps/web/components/app/messages/MessagePresentation.tsx create mode 100644 apps/web/components/app/messages/message-utils.ts create mode 100644 apps/web/lib/utils/useClipboardFeedback.ts create mode 100644 packages/cli/src/commands/push-output.ts diff --git a/apps/web/app/(app)/post/new/CopyCommand.tsx b/apps/web/app/(app)/post/new/CopyCommand.tsx index 1c99f6c8..0b203486 100644 --- a/apps/web/app/(app)/post/new/CopyCommand.tsx +++ b/apps/web/app/(app)/post/new/CopyCommand.tsx @@ -1,50 +1,26 @@ "use client"; -import { useCallback, useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { useClipboardFeedback } from "@/lib/utils/useClipboardFeedback"; export function CopyCommand({ command }: { command: string }) { - const [copied, setCopied] = useState(false); - - const copy = useCallback(() => { - navigator.clipboard.writeText(command).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }, [command]); + const { copied, copyText } = useClipboardFeedback(); return ( ); diff --git a/apps/web/components/app/feed/FeedList.tsx b/apps/web/components/app/feed/FeedList.tsx index b1ced9c2..850c3c9a 100644 --- a/apps/web/components/app/feed/FeedList.tsx +++ b/apps/web/components/app/feed/FeedList.tsx @@ -6,19 +6,13 @@ import { ChevronDown, Copy, Check } from "lucide-react"; import { ActivityCard } from "./ActivityCard"; import { PendingPostsNudge } from "./PendingPostsNudge"; import { cn } from "@/lib/utils/cn"; +import { useClipboardFeedback } from "@/lib/utils/useClipboardFeedback"; import type { Post } from "@/types"; const SYNC_COMMAND = "npx straude@latest"; function SyncCommandHint() { - const [copied, setCopied] = useState(false); - - function handleCopy() { - navigator.clipboard.writeText(SYNC_COMMAND).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - } + const { copied, copyText } = useClipboardFeedback(); return (
@@ -27,7 +21,7 @@ function SyncCommandHint() { + ))} +
+ )} + + {fileAttachments.length > 0 && ( +
+ {fileAttachments.map((attachment) => ( + + ))} +
+ )} + + {message.content && ( +

+ {message.content} +

+ )} + +
+ {timeAgo(message.created_at)} + {mine && message.delivery_status && ( + {message.delivery_status} + )} +
+ + + ); +} + +export function PendingAttachmentList({ + attachments, + onRemove, +}: { + attachments: PendingAttachment[]; + onRemove: (index: number) => void; +}) { + if (attachments.length === 0) return null; + + return ( +
+ {attachments.map((attachment, index) => ( +
+ {attachment.preview ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {attachment.file.name} + {attachment.uploading && ( +
+ +
+ )} +
+ ) : ( +
+
+ )} + {!attachment.uploading && ( + + )} +
+ ))} +
+ ); +} + +export function MessageImageLightbox({ + imageUrl, + onClose, +}: { + imageUrl: string; + onClose: () => void; +}) { + return ( +
{ + if (event.key === "Escape") onClose(); + }} + > + +
event.stopPropagation()} + > + +
+
+ ); +} diff --git a/apps/web/components/app/messages/MessagesInbox.tsx b/apps/web/components/app/messages/MessagesInbox.tsx index 3e583fa4..87eebde2 100644 --- a/apps/web/components/app/messages/MessagesInbox.tsx +++ b/apps/web/components/app/messages/MessagesInbox.tsx @@ -1,6 +1,5 @@ "use client"; -import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -9,7 +8,7 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; -import { ArrowLeft, FileText, Loader2, MessageSquare, Paperclip, X } from "lucide-react"; +import { ArrowLeft, Loader2, MessageSquare, Paperclip } from "lucide-react"; import { Avatar } from "@/components/ui/Avatar"; import { Button } from "@/components/ui/Button"; import { Textarea } from "@/components/ui/Textarea"; @@ -24,6 +23,18 @@ import type { DirectMessageThread, MessageAttachmentInput, } from "@/types"; +import { + MessageBubble, + MessageImageLightbox, + PendingAttachmentList, +} from "./MessagePresentation"; +import { + createPendingAttachment, + isImageMime, + threadPreview, + type LocalDirectMessage, + type PendingAttachment, +} from "./message-utils"; interface ConversationUser { id: string; @@ -32,12 +43,6 @@ interface ConversationUser { display_name: string | null; } -interface PendingAttachment { - file: File; - preview?: string; - uploading: boolean; -} - interface ThreadsResponse { threads: DirectMessageThread[]; unread_count: number; @@ -50,12 +55,6 @@ interface ConversationResponse { has_more?: boolean; } -type DeliveryStatus = "sending" | "sent" | "failed"; - -type LocalDirectMessage = DirectMessage & { - delivery_status?: DeliveryStatus; -}; - interface SendMessageVariables { username: string; content: string; @@ -121,32 +120,6 @@ function updateThreadAfterSend( }; } -function formatFileSize(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; -} - -const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "webp", "gif", "heic", "heif"]; - -function isImageMime(type: string, fileName?: string): boolean { - if (type.startsWith("image/")) return true; - if (!fileName) return false; - const ext = fileName.split(".").pop()?.toLowerCase(); - return !!ext && IMAGE_EXTENSIONS.includes(ext); -} - -function threadPreview(thread: DirectMessageThread): string { - const prefix = thread.last_message_is_from_me ? "You: " : ""; - if (thread.last_message_content) { - return `${prefix}${thread.last_message_content}`; - } - if (thread.last_message_has_attachment) { - return `${prefix}Sent an attachment`; - } - return ""; -} - export function MessagesInbox({ initialUsername, initialThreads, @@ -421,12 +394,7 @@ export function MessagesInbox({ if (remaining <= 0) return; const toAdd = files.slice(0, remaining); - const newAttachments: PendingAttachment[] = toAdd.map((file) => { - const preview = isImageMime(file.type, file.name) - ? URL.createObjectURL(file) - : undefined; - return { file, preview, uploading: false }; - }); + const newAttachments = toAdd.map(createPendingAttachment); setPendingAttachments((prev) => [...prev, ...newAttachments]); } @@ -880,112 +848,14 @@ export function MessagesInbox({ )} - {messages.map((message) => { - const mine = message.sender_id === currentUserId; - const attachments = message.attachments ?? []; - const imageAttachments = attachments.filter((a) => isImageMime(a.type, a.name)); - const fileAttachments = attachments.filter((a) => !isImageMime(a.type, a.name)); - - return ( -
( + -
- {/* Image attachments */} - {imageAttachments.length > 0 && ( -
- {imageAttachments.map((attachment) => ( - - ))} -
- )} - - {/* File attachments */} - {fileAttachments.length > 0 && ( -
- {fileAttachments.map((attachment) => ( - - - ))} -
- )} - - {/* Text content */} - {message.content && ( -

- {message.content} -

- )} - -
- - {timeAgo(message.created_at)} - - {mine && message.delivery_status && ( - - {message.delivery_status} - - )} -
-
-
- ); - })} + message={message} + currentUserId={currentUserId} + onOpenImage={setLightboxImage} + /> + ))} )}
@@ -1010,65 +880,10 @@ export function MessagesInbox({ Message @{counterpart.username} - {pendingAttachments.length > 0 && ( -
- {pendingAttachments.map((attachment, index) => ( -
- {attachment.preview ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {attachment.file.name} - {attachment.uploading && ( -
- -
- )} -
- ) : ( -
-
- )} - {!attachment.uploading && ( - - )} -
- ))} -
- )} +