Clone do WhatsApp construído sobre shadcn/ui (Mira · Neutro · Geist · radius Small) - #2
Conversation
Full WhatsApp Web-style app in Next.js (App Router) + Tailwind v4, with the UI built on shadcn/ui following the official docs. Scaffolded with `npx shadcn create` using the Mira style, Neutral base color, Geist font and Small radius. Features: - Chat list: search, filters (all/unread/favourites/groups), pinned & muted chats, drafts, unread counts and read receipts in previews. - Conversation: presence, date separators, bubble tails, message grouping, delivery receipts (sent/delivered/read) and encryption notice. - Real message sending with simulated typing indicator and auto-reply. - Light/dark mode via next-themes; responsive list/conversation switch. Config aligned to spec: --radius set to 0.375rem (Small) and Geist wired as the sans/heading font (replacing the preset's Inter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011P9yhE74yqqUHnVzs6nmwM
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdded a complete Portuguese WhatsApp Web clone with Next.js, Tailwind CSS, reusable UI components, seeded local data, browser persistence, chat and call workflows, responsive layouts, and Playwright end-to-end tests. ChangesWhatsApp Web clone
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Migrates the hand-rolled conversation UI onto the chat primitives the shadcn registry actually provides, then builds out the features that were missing. Correction — use the official primitives instead of hand-written equivalents: - bubble (+ BubbleReactions), message, marker, attachment and message-scroller replace the custom message markup, the ad-hoc separators and the manual scrollIntoView. The scroll-to-end FAB now comes from the primitive. - Fonts installed via the registry's @shadcn/font-geist and @shadcn/font-heading-geist, so --font-sans and --font-heading are both Geist. Features: - Reply/quote with jump-to-original, emoji reactions, forward, star, pin, delete tombstones, multi-select with an action bar. - Inline formatting (*bold*, _italic_, ~strike~, `mono`) and autolinking, rendered as React nodes rather than injected HTML. - Media messages: image with lightbox, video, voice notes with waveform, scrubbing and speed, documents, location and contact. - Composer: emoji picker, attachments, simulated voice recording, drafts. - Nav rail, contact/group info panel, in-chat search, new-chat picker, archived view, calls history, communities and settings; QR pairing at /connect. Status and Canais are intentionally out of scope. - State moved to a reducer + context with localStorage persistence, so conversations survive a reload. Unread count mirrored in the tab title. Fixes found while verifying: - CommandDialog rendered its children outside the Command context, so every cmdk primitive threw on an undefined store. Wrapped them. - The global Escape shortcut also closed the conversation when dismissing a dialog or sheet. It now bails on defaultPrevented, which is the only reliable signal since Radix unmounts the overlay before the handler runs. - Reaction pills overhang the bubble and collided with the next message. - Spinner spread svg props into an icon typed for numeric strokeWidth. Verified with typecheck, lint, production build, and a Playwright pass over both themes at desktop and mobile widths with no console or page errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011P9yhE74yqqUHnVzs6nmwM
An audit of the app found that the presentation layer was solid but the behaviour layer had holes: flows that dead-ended, capability with no way to reach it, and state that corrupted with continued use. State bugs fixed: - The composer was not derived from the conversation. It never loaded the chat's draft, carried typed text into the next chat, and — because the debounce depended on [value, chat.id] — stamped the previous chat's text as the new chat's draft. Now keyed by chat and seeded from the draft. - `typing` was persisted, so reloading mid-simulation left a chat stuck on "digitando…" forever. Transient state is stripped before serialising. - Forward derived ids from the source message, colliding when the same message was forwarded twice to the same chat. Ids now come from a shared generator. - Calls and communities were module constants outside the store, so deleting a chat left call rows pointing at nothing. Both moved into the store and pruned on delete. - Pending reply timers were never removed from their array. Dead ends now finish: - Simulated call experience: dialling → connected → hung up, with a real timer, mute/camera/speaker, and a history entry carrying the actual duration. Wired to the conversation header and the call history. - Block/report with real effects — a blocked chat disables the composer and appears in Settings. - Downloads produce real files: images save their data URI, documents get a generated PDF with a valid xref table. - Settings preferences now do something: read receipts govern the blue tick, notifications govern the toasts. Capability opened up: - EDIT_MESSAGE existed in the reducer with no trigger, making the "editada" label unreachable. Editing is now in the message menu. - Chat search covers the whole transcript instead of only the last message. - Removed five installed-but-unused registry components. Verification is now reproducible: Playwright suite under e2e/ with `npm run test:e2e`, 19 specs across desktop and mobile. Because the reply simulation uses timers and Math.random, `?e2e=1` disables it so assertions are deterministic. Includes regressions for the draft leak and the stuck typing indicator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011P9yhE74yqqUHnVzs6nmwM
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (11)
whatsapp/components/whatsapp/use-send-message.ts (1)
20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the import to the top and drop the re-export.
The mid-file import breaks the import block. The re-export adds a second public path to
nextIdand hides@/lib/idas the owner. Update the callers that importnextIdfrom this module.♻️ Proposed change
import { isSimulationDisabled } from "`@/lib/e2e`" +import { nextId } from "`@/lib/id`" import { useStore } from "`@/lib/store`"-import { nextId } from "`@/lib/id`" - -export { nextId } -🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/use-send-message.ts` around lines 20 - 22, Update callers currently importing nextId through use-send-message.ts to import it directly from `@/lib/id`, then remove the nextId re-export and relocate the remaining import to the module’s top import block.whatsapp/lib/storage.ts (1)
31-46: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winValidate each chat entry, not just the array.
The check accepts any non-empty array. A stored entry without
conversationoridthen reaches the renderer and throws, and the seed fallback never runs. Validate the required fields of each chat and returnnullwhen one entry fails.♻️ Proposed per-entry validation
- if (!parsed || !Array.isArray(parsed.chats) || !parsed.chats.length) { + const isChat = (c: unknown): c is PersistedState["chats"][number] => + typeof c === "object" && + c !== null && + typeof (c as { id?: unknown }).id === "string" && + Array.isArray((c as { conversation?: unknown }).conversation) + if ( + !parsed || + !Array.isArray(parsed.chats) || + !parsed.chats.length || + !parsed.chats.every(isChat) + ) { return null }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/lib/storage.ts` around lines 31 - 46, Update the persisted-state validation around parsed.chats to inspect every chat entry, requiring each item to have valid conversation and id fields before returning the normalized state. Return null immediately when any entry fails validation, while preserving the existing seed fallback and normalization for valid chat arrays.whatsapp/lib/store.tsx (2)
354-356: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
clearStored()out of the reducer.The reducer must stay pure. React can call it more than once for the same action, so storage removal is not tied to a committed state. The call is also redundant:
hydratedstaystrue, so the persist effect at Lines 378-394 immediately writes the seed back under the same key. DispatchRESETfrom a handler that callsclearStored()first, or let the persist effect alone restore the seed.♻️ Proposed change
case "RESET": - clearStored() return { ...initialState, hydrated: true }Then clear storage at the call site in
whatsapp/components/whatsapp/settings-view.tsx:+ clearStored() dispatch({ type: "RESET" })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/lib/store.tsx` around lines 354 - 356, Remove the clearStored() side effect from the RESET branch of the reducer, keeping the reducer limited to returning the reset state. Update the RESET dispatch flow in settings-view.tsx to call clearStored() before dispatching RESET, or rely solely on the existing persistence effect to restore the seed; ensure storage clearing is not performed inside the reducer.
378-394: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDebounce persistence, because drafts write on every keystroke.
SET_DRAFTreplaces the chat object, sostate.chatschanges for each typed character. This effect then serializes all chats and writes tolocalStoragesynchronously on the typing path. The cost grows with conversation size. Debounce the write, or excludedraftfrom the persisted payload.♻️ Proposed debounce
React.useEffect(() => { if (!state.hydrated) return - saveState({ - chats: state.chats, - calls: state.calls, - communities: state.communities, - blocked: state.blocked, - preferences: state.preferences, - }) + const handle = window.setTimeout(() => { + saveState({ + chats: state.chats, + calls: state.calls, + communities: state.communities, + blocked: state.blocked, + preferences: state.preferences, + }) + }, 300) + return () => window.clearTimeout(handle) }, [🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/lib/store.tsx` around lines 378 - 394, Debounce the persistence effect that saves state through saveState, preventing SET_DRAFT-driven state.chats changes from synchronously serializing and writing all chats on every keystroke. Preserve the hydrated guard and existing persisted fields, while ensuring the debounce is cancelled or cleaned up when dependencies change or the effect unmounts.whatsapp/components/whatsapp/settings-view.tsx (1)
132-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
next/linkfor the/connectnavigation.
/connectis an internal App Router route, so replace the plain<a>withLinkto keep navigation client-side and preserve React state across the page load.♻️ Proposed change
+import Link from "next/link"<Button variant="outline" size="sm" asChild> - <a href="/connect">Abrir</a> + <Link href="/connect">Abrir</Link> </Button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/settings-view.tsx` around lines 132 - 135, Replace the plain anchor in the settings view’s `/connect` Button with Next.js `Link`, adding the required import and preserving the existing Button props and link label so internal navigation remains client-side.Source: Coding guidelines
whatsapp/components/whatsapp/message-actions.tsx (1)
37-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused
isGroupprop.
MessageActionsPropsdeclaresisGroup, but the component never destructures or uses it. The prop widens the public contract without effect. Remove it, or use it to gate group-only actions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/message-actions.tsx` around lines 37 - 48, Remove the unused isGroup property from the MessageActionsProps interface and update any callers of the MessageActions component to stop passing it, leaving the component behavior unchanged.whatsapp/components/whatsapp/forward-dialog.tsx (1)
92-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose the recipient selection state to assistive technology.
The button communicates selection through background color and a check mark only. Screen reader users cannot determine which recipients are selected. Add
aria-pressed.♿ Proposed fix
<button key={chat.id} type="button" + aria-pressed={isPicked} onClick={() =>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/forward-dialog.tsx` around lines 92 - 125, Add an aria-pressed attribute to the recipient selection button in the forward-dialog mapping, using the existing isPicked boolean so assistive technology receives the current selection state while preserving the existing toggle behavior.whatsapp/components/whatsapp/audio-message.tsx (1)
30-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
setPlaying(false)out of thesetElapsedupdater.
setElapsedreceives a pure updater function. CallingsetPlaying(false)inside that function makes the updater cause a state update during its evaluation, so strict rendering or replayed updater evaluations can apply the stop behavior multiple times. Track completion directly in the interval callback or with a ref, then update both state values from the effects handler.♻️ Proposed refactor
+ const elapsedRef = React.useRef(0) + elapsedRef.current = elapsed + React.useEffect(() => { if (!playing) return const id = window.setInterval(() => { - setElapsed((e) => { - const next = e + 0.1 * speed - if (next >= message.duration) { - setPlaying(false) - return 0 - } - return next - }) + const next = elapsedRef.current + 0.1 * speed + if (next >= message.duration) { + setPlaying(false) + setElapsed(0) + return + } + setElapsed(next) }, 100) return () => window.clearInterval(id) }, [playing, speed, message.duration])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/audio-message.tsx` around lines 30 - 41, Move the completion side effect out of the setElapsed updater in the interval created by the playing effect. Determine whether the next elapsed value reaches message.duration in the interval callback (using a ref if needed), then call setPlaying(false) there and keep the setElapsed updater pure, preserving the reset-to-zero completion behavior.whatsapp/components/whatsapp/call-overlay.tsx (1)
126-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd focus management to the call overlay.
The overlay declares
role="dialog"andaria-modal="true", but it does not move focus into itself and does not trap focus. Keyboard users stay on the element behind the overlay, and Tab reaches the chat list and composer under it. Move focus to the hang-up button on mount, and restore focus on unmount. A RadixDialogprimitive already provides this behavior and is available in this project.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/call-overlay.tsx` around lines 126 - 132, Update the call overlay component around the target-rendering dialog to use the available Radix Dialog primitive, ensuring focus moves to the hang-up button when the overlay mounts, Tab focus is trapped within the modal, and the previously focused element is restored when it unmounts.whatsapp/components/whatsapp/chat-list.tsx (1)
148-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the shared
Inputprimitive for the search field.This field is a raw
<input>with inline utility classes. The project providesInputinwhatsapp/components/ui/input.tsx, and the rest of this cohort uses the shared primitives. Replacing the raw element keeps the focus ring, border, and dark-mode styles in one place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/chat-list.tsx` around lines 148 - 154, Replace the raw input in the chat-list search field with the shared Input primitive imported from the UI input module, preserving its value, change handler, placeholder, aria-label, and existing styling props so the current search behavior and appearance remain unchanged.whatsapp/components/whatsapp/nav-rail.tsx (1)
43-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnounce the unread count on the chats control.
The badge conveys the unread count visually only.
aria-labelstays"Conversas", so screen reader users get no count. Include the count in the accessible name, and hide the visual badge from assistive technology.♿ Proposed fix: include the count in the accessible name
<Button variant="ghost" size="icon-lg" - aria-label={entry.label} + aria-label={ + entry.view === "chats" && unreadTotal > 0 + ? `${entry.label}, ${unreadTotal} não lidas` + : entry.label + } aria-current={active ? "page" : undefined} @@ {entry.view === "chats" && unreadTotal > 0 ? ( - <span className="absolute top-1 right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[0.5625rem] font-semibold text-primary-foreground"> + <span + aria-hidden="true" + className="absolute top-1 right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[0.5625rem] font-semibold text-primary-foreground" + >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/nav-rail.tsx` around lines 43 - 63, Update the chats Button in the nav-rail entry mapping to include the unread count in its accessible name when unreadTotal is greater than zero, while preserving the existing label otherwise. Mark the visual unread badge span as hidden from assistive technology with the appropriate accessibility attribute to avoid announcing the count twice.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@whatsapp/components/ui/collapsible.tsx`:
- Around line 5-25: Add the explicit type-only React namespace import before the
React type references in whatsapp/components/ui/collapsible.tsx lines 5-25,
whatsapp/components/ui/sonner.tsx lines 8-38, and
whatsapp/components/ui/spinner.tsx lines 5-10; no other changes are required.
In `@whatsapp/components/ui/input-group.tsx`:
- Around line 57-62: Update the InputGroupAddon onClick handler to locate and
focus either supported form control, input or textarea, instead of querying only
input. Preserve the existing button exclusion and currentTarget parent lookup
behavior.
In `@whatsapp/components/ui/switch.tsx`:
- Around line 20-27: Update the Switch track and Thumb class selectors to use
Radix’s data-state attributes: replace data-checked/data-unchecked selectors
with data-[state=checked]/data-[state=unchecked], including the grouped thumb
translation selectors, while preserving the existing checked and unchecked
styling behavior.
In `@whatsapp/components/whatsapp/call-overlay.tsx`:
- Around line 107-119: Update the Escape key listener in the React.useEffect
associated with call overlay hang-up to register in the capture phase, ensuring
e.preventDefault() runs before Shell’s window keydown listener observes the
event. Keep the existing hangUp behavior and cleanup, and pass matching
capture-phase options when adding and removing the listener.
In `@whatsapp/components/whatsapp/communities-view.tsx`:
- Around line 35-39: Update the community toggle button in the communities view
to expose its expanded state with aria-expanded={open} and aria-controls
referencing the collapsible panel. Assign the matching id to the expanded panel
element, or use the existing Collapsible component from the UI library to wire
both attributes consistently.
In `@whatsapp/components/whatsapp/contact-panel.tsx`:
- Around line 177-180: Update the CollapsibleTrigger containing the
ChevronDownIcon to include the group class, then change the Icon’s rotation
utility to the group data-state variant so it responds to the trigger’s open
state. Preserve the existing icon styling and transition behavior.
- Around line 130-146: Update the media grid rendering in the contact panel so
image messages continue using the existing img element, while video messages use
a valid thumbnail field or a video element with preload="metadata" instead of
passing VideoMessage.url to img. Preserve the existing media limit, layout, and
key handling.
In `@whatsapp/components/whatsapp/conversation-search.tsx`:
- Around line 45-54: The CommandItem values are not guaranteed unique, so
prepend each item’s stable id to the value. Update the CommandItem in
whatsapp/components/whatsapp/conversation-search.tsx (lines 45-54) to include
r.id before r.text and r.time, and update both CommandItem value props in
whatsapp/components/whatsapp/new-chat-dialog.tsx (lines 44-79) similarly.
In `@whatsapp/components/whatsapp/conversation.tsx`:
- Around line 266-279: Update the bulk “Favoritar” action in the
selected-message Button to apply one consistent starred state to all selected
messages instead of toggling each individually. Pass an explicit target value or
update the STAR_MESSAGES reducer to derive it from whether every selected
message is already starred, preserving the labeled intent to star the entire
selection.
- Around line 404-416: Update the archive action’s DropdownMenuItem to use the
existing ArchiveIcon export instead of ForwardIcon, adding or adjusting the
import as needed while leaving the CHAT_FLAG dispatch behavior unchanged.
In `@whatsapp/components/whatsapp/message-actions.tsx`:
- Around line 63-67: Update the copy function to handle the promise returned by
navigator.clipboard.writeText instead of discarding it, catching failures to
provide user feedback and confirming successful copies through the existing
notification mechanism.
In `@whatsapp/components/whatsapp/new-chat-dialog.tsx`:
- Line 43: Update the CommandEmpty text in the new chat dialog to mention both
contacts and groups, using wording such as “Nenhum contato ou grupo
encontrado.”.
In `@whatsapp/components/whatsapp/status-ticks.tsx`:
- Around line 25-32: Update the className composition in the Icon within the
status rendering to always merge the incoming className with the status-specific
classes. Keep the read color classes later in the cn arguments so tailwind-merge
overrides caller text colors while preserving sizing, spacing, and other
utilities.
In `@whatsapp/components/whatsapp/theme-toggle.tsx`:
- Around line 21-36: Update the theme-toggle render logic around isDark so it
includes the mounted state, using mounted && resolvedTheme === "dark" for the
aria-label, TooltipContent text, and Icon selection. Keep the pre-mount markup
consistently on the light-theme action/icon while preserving the existing
toggling behavior.
In `@whatsapp/components/whatsapp/use-send-message.ts`:
- Around line 43-46: Move the simulated-reply timer scheduling out of
useSendMessage and ChatComposer into state or a component that remains mounted
across chat selection, such as the shared WhatsappApp/Conversation flow.
Preserve the pending reply and cross-chat toast behavior when switching chats,
ensuring composer unmounts do not cancel the timers.
In `@whatsapp/e2e/app.spec.ts`:
- Around line 41-46: Update the test “never restores a stuck typing indicator”
to seed localStorage with a valid persisted application state containing typing:
true before navigation or initialization, rather than only inspecting the
default state. After hydration, assert the typing indicator is cleared in the UI
and that the stored whatsapp-shadcn:state:v2 value no longer contains typing:
true.
In `@whatsapp/lib/download.ts`:
- Around line 45-65: Update the PDF generation flow around the text assembly and
xref construction to emit an ASCII-only payload before calculating offsets,
including sanitizing non-ASCII characters in title and lines so Helvetica can
render the content. Compute stream length, object offsets, and startxref from
the UTF-8 byte representation used by downloadBlob rather than JavaScript string
lengths, while preserving the existing PDF structure.
---
Nitpick comments:
In `@whatsapp/components/whatsapp/audio-message.tsx`:
- Around line 30-41: Move the completion side effect out of the setElapsed
updater in the interval created by the playing effect. Determine whether the
next elapsed value reaches message.duration in the interval callback (using a
ref if needed), then call setPlaying(false) there and keep the setElapsed
updater pure, preserving the reset-to-zero completion behavior.
In `@whatsapp/components/whatsapp/call-overlay.tsx`:
- Around line 126-132: Update the call overlay component around the
target-rendering dialog to use the available Radix Dialog primitive, ensuring
focus moves to the hang-up button when the overlay mounts, Tab focus is trapped
within the modal, and the previously focused element is restored when it
unmounts.
In `@whatsapp/components/whatsapp/chat-list.tsx`:
- Around line 148-154: Replace the raw input in the chat-list search field with
the shared Input primitive imported from the UI input module, preserving its
value, change handler, placeholder, aria-label, and existing styling props so
the current search behavior and appearance remain unchanged.
In `@whatsapp/components/whatsapp/forward-dialog.tsx`:
- Around line 92-125: Add an aria-pressed attribute to the recipient selection
button in the forward-dialog mapping, using the existing isPicked boolean so
assistive technology receives the current selection state while preserving the
existing toggle behavior.
In `@whatsapp/components/whatsapp/message-actions.tsx`:
- Around line 37-48: Remove the unused isGroup property from the
MessageActionsProps interface and update any callers of the MessageActions
component to stop passing it, leaving the component behavior unchanged.
In `@whatsapp/components/whatsapp/nav-rail.tsx`:
- Around line 43-63: Update the chats Button in the nav-rail entry mapping to
include the unread count in its accessible name when unreadTotal is greater than
zero, while preserving the existing label otherwise. Mark the visual unread
badge span as hidden from assistive technology with the appropriate
accessibility attribute to avoid announcing the count twice.
In `@whatsapp/components/whatsapp/settings-view.tsx`:
- Around line 132-135: Replace the plain anchor in the settings view’s
`/connect` Button with Next.js `Link`, adding the required import and preserving
the existing Button props and link label so internal navigation remains
client-side.
In `@whatsapp/components/whatsapp/use-send-message.ts`:
- Around line 20-22: Update callers currently importing nextId through
use-send-message.ts to import it directly from `@/lib/id`, then remove the nextId
re-export and relocate the remaining import to the module’s top import block.
In `@whatsapp/lib/storage.ts`:
- Around line 31-46: Update the persisted-state validation around parsed.chats
to inspect every chat entry, requiring each item to have valid conversation and
id fields before returning the normalized state. Return null immediately when
any entry fails validation, while preserving the existing seed fallback and
normalization for valid chat arrays.
In `@whatsapp/lib/store.tsx`:
- Around line 354-356: Remove the clearStored() side effect from the RESET
branch of the reducer, keeping the reducer limited to returning the reset state.
Update the RESET dispatch flow in settings-view.tsx to call clearStored() before
dispatching RESET, or rely solely on the existing persistence effect to restore
the seed; ensure storage clearing is not performed inside the reducer.
- Around line 378-394: Debounce the persistence effect that saves state through
saveState, preventing SET_DRAFT-driven state.chats changes from synchronously
serializing and writing all chats on every keystroke. Preserve the hydrated
guard and existing persisted fields, while ensuring the debounce is cancelled or
cleaned up when dependencies change or the effect unmounts.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 969c2b4f-f569-4988-8197-99185aecb253
⛔ Files ignored due to path filters (2)
whatsapp/app/favicon.icois excluded by!**/*.icowhatsapp/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (89)
README.mdwhatsapp/.gitignorewhatsapp/.prettierignorewhatsapp/.prettierrcwhatsapp/AGENTS.mdwhatsapp/README.mdwhatsapp/app/connect/page.tsxwhatsapp/app/globals.csswhatsapp/app/layout.tsxwhatsapp/app/page.tsxwhatsapp/components.jsonwhatsapp/components/.gitkeepwhatsapp/components/theme-provider.tsxwhatsapp/components/ui/attachment.tsxwhatsapp/components/ui/avatar.tsxwhatsapp/components/ui/badge.tsxwhatsapp/components/ui/bubble.tsxwhatsapp/components/ui/button.tsxwhatsapp/components/ui/collapsible.tsxwhatsapp/components/ui/command.tsxwhatsapp/components/ui/context-menu.tsxwhatsapp/components/ui/dialog.tsxwhatsapp/components/ui/dropdown-menu.tsxwhatsapp/components/ui/empty.tsxwhatsapp/components/ui/input-group.tsxwhatsapp/components/ui/input.tsxwhatsapp/components/ui/item.tsxwhatsapp/components/ui/marker.tsxwhatsapp/components/ui/message-scroller.tsxwhatsapp/components/ui/message.tsxwhatsapp/components/ui/popover.tsxwhatsapp/components/ui/separator.tsxwhatsapp/components/ui/sheet.tsxwhatsapp/components/ui/skeleton.tsxwhatsapp/components/ui/slider.tsxwhatsapp/components/ui/sonner.tsxwhatsapp/components/ui/spinner.tsxwhatsapp/components/ui/switch.tsxwhatsapp/components/ui/textarea.tsxwhatsapp/components/ui/tooltip.tsxwhatsapp/components/whatsapp/audio-message.tsxwhatsapp/components/whatsapp/call-overlay.tsxwhatsapp/components/whatsapp/calls-view.tsxwhatsapp/components/whatsapp/chat-composer.tsxwhatsapp/components/whatsapp/chat-list-item.tsxwhatsapp/components/whatsapp/chat-list.tsxwhatsapp/components/whatsapp/communities-view.tsxwhatsapp/components/whatsapp/contact-panel.tsxwhatsapp/components/whatsapp/conversation-search.tsxwhatsapp/components/whatsapp/conversation.tsxwhatsapp/components/whatsapp/emoji-picker.tsxwhatsapp/components/whatsapp/empty-conversation.tsxwhatsapp/components/whatsapp/forward-dialog.tsxwhatsapp/components/whatsapp/icon.tsxwhatsapp/components/whatsapp/icons.tswhatsapp/components/whatsapp/media-message.tsxwhatsapp/components/whatsapp/message-actions.tsxwhatsapp/components/whatsapp/message-bubble.tsxwhatsapp/components/whatsapp/nav-rail.tsxwhatsapp/components/whatsapp/new-chat-dialog.tsxwhatsapp/components/whatsapp/rich-text.tsxwhatsapp/components/whatsapp/settings-view.tsxwhatsapp/components/whatsapp/status-ticks.tsxwhatsapp/components/whatsapp/theme-toggle.tsxwhatsapp/components/whatsapp/use-send-message.tswhatsapp/components/whatsapp/whatsapp-app.tsxwhatsapp/e2e/app.spec.tswhatsapp/e2e/helpers.tswhatsapp/e2e/messaging.spec.tswhatsapp/eslint.config.mjswhatsapp/hooks/.gitkeepwhatsapp/hooks/use-hydrated.tswhatsapp/lib/.gitkeepwhatsapp/lib/data.tswhatsapp/lib/download.tswhatsapp/lib/e2e.tswhatsapp/lib/format.tswhatsapp/lib/id.tswhatsapp/lib/media.tswhatsapp/lib/storage.tswhatsapp/lib/store.tsxwhatsapp/lib/types.tswhatsapp/lib/utils.tswhatsapp/next.config.tswhatsapp/package.jsonwhatsapp/playwright.config.tswhatsapp/postcss.config.mjswhatsapp/public/.gitkeepwhatsapp/tsconfig.json
Worked through the 17 actionable comments and 11 nitpicks, verifying each against the code rather than applying them blindly. Confirmed bugs: - switch.tsx styled on data-checked/data-unchecked, but the installed Radix emits data-state. Verified against the package source: the track colour and thumb position never changed. Now data-[state=*]; confirmed in a browser that the background actually changes on toggle. - Escape during a call also closed the conversation behind it. Shell registers its window listener first, so it observed the event before preventDefault ran here; this listener now uses the capture phase. - Simulated reply timers were tied to the composer, which is keyed by chat, so switching conversations cancelled every pending reply — making the cross-chat notification unreachable. Timers moved to module scope, flushed on pagehide. - buildPdf computed /Length and xref offsets from String#length while the Blob encodes UTF-8, so any accented filename desynced them and produced a file readers reject. Payload folded to ASCII, which also suits Type1 Helvetica. Verified byte-for-byte with an accented input. - Bulk "Favoritar" toggled each message, so a mixed selection unstarred part of it. STAR_MESSAGES now takes an explicit target value. - clearStored() ran inside the reducer; moved to the call site. - Chevron in the contact panel never rotated: data-state sits on the trigger. - Archive menu item showed ForwardIcon. - Clipboard rejection was discarded; now reports success or failure. - setPlaying was called inside a setElapsed updater. - cmdk items could share a value; ids now make them unique. - Persisted state was accepted on array shape alone; each chat is validated. - The typing-indicator test passed without exercising the regression. It now seeds storage with typing:true and asserts hydration clears it. Also: debounced persistence (drafts wrote on every keystroke) with a pagehide flush so the debounce cannot cost data on reload; focus management for the call overlay; a11y state on the nav rail, forward picker and communities; next/link for /connect; shared Input primitive in the chat list; explicit React type imports; dropped a dead isGroup prop. Skipped: the 80% docstring-coverage check. This code comments decisions rather than restating signatures, and padding every function to hit a ratio would add noise, not clarity. Verified: typecheck, lint, production build, and the full Playwright suite — 38/38 across desktop and mobile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011P9yhE74yqqUHnVzs6nmwM
|
Trabalhei os 17 comentários acionáveis e os 11 nitpicks, verificando cada um contra o código em vez de aplicar direto. Resumo em Confirmados e corrigidosEstes eu validei empiricamente antes de mexer:
O comentário sobre o teste de Uma correção que gerou regressão, pega pela suíteApliquei o debounce sugerido na persistência e 10 testes quebraram: recarregar logo após digitar perdia a escrita. O debounce estava certo em intenção, mas abria janela de perda de dados. Mantive o debounce e adicionei flush em Não aplicadoCobertura de docstrings (6,25% vs 80%). Este código comenta decisões — por que o Escape usa Sobre a sugestão de gerar testes unitários: seria redundante com a suíte E2E de 38 testes já versionada em Verificação
Generated by Claude Code |
|
A review do Uma nota sobre a única thread que não foi marcada como resolvida, em @coderabbitai review Generated by Claude Code |
|
Vou executar uma nova review do PR. ✏️ Learnings added
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
whatsapp/components/whatsapp/use-send-message.ts (1)
46-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRead the current notification preference in the delayed callback.
notifyis captured when the user sends the message. If the user disables notifications before the simulated reply runs, Line 116 can still show a toast. Store the preference in a ref that updates with state, then read that ref in the delayed callback.Proposed fix
- const notify = state.preferences.notifications + const notificationsRef = React.useRef(state.preferences.notifications) const selectedIdRef = React.useRef(state.selectedId) React.useEffect(() => { selectedIdRef.current = state.selectedId - }, [state.selectedId]) + notificationsRef.current = state.preferences.notifications + }, [state.selectedId, state.preferences.notifications]) ... - if (notify && chatId !== selectedIdRef.current) { + if (notificationsRef.current && chatId !== selectedIdRef.current) { toast(chat.name, { description: reply.text }) } ... - [chat.id, chat.isGroup, chat.members, chat.name, dispatch, later, notify, state.replyToId] + [chat.id, chat.isGroup, chat.members, chat.name, dispatch, later, state.replyToId]Also applies to: 114-118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@whatsapp/components/whatsapp/use-send-message.ts` around lines 46 - 53, Update the delayed callback in the message-sending flow to read the current notification preference through a ref rather than the captured notify value. Add a notification-preference ref alongside selectedIdRef, keep it synchronized in the existing state-update effect, and use that ref at the toast decision around the delayed callback’s notification handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@whatsapp/components/whatsapp/use-send-message.ts`:
- Around line 46-53: Update the delayed callback in the message-sending flow to
read the current notification preference through a ref rather than the captured
notify value. Add a notification-preference ref alongside selectedIdRef, keep it
synchronized in the existing state-update effect, and use that ref at the toast
decision around the delayed callback’s notification handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d0f0c1b3-ca44-45d6-a8ac-9e51818cc5f6
📒 Files selected for processing (26)
whatsapp/components/ui/collapsible.tsxwhatsapp/components/ui/input-group.tsxwhatsapp/components/ui/sonner.tsxwhatsapp/components/ui/spinner.tsxwhatsapp/components/ui/switch.tsxwhatsapp/components/whatsapp/audio-message.tsxwhatsapp/components/whatsapp/call-overlay.tsxwhatsapp/components/whatsapp/chat-list.tsxwhatsapp/components/whatsapp/communities-view.tsxwhatsapp/components/whatsapp/contact-panel.tsxwhatsapp/components/whatsapp/conversation-search.tsxwhatsapp/components/whatsapp/conversation.tsxwhatsapp/components/whatsapp/forward-dialog.tsxwhatsapp/components/whatsapp/message-actions.tsxwhatsapp/components/whatsapp/message-bubble.tsxwhatsapp/components/whatsapp/nav-rail.tsxwhatsapp/components/whatsapp/new-chat-dialog.tsxwhatsapp/components/whatsapp/settings-view.tsxwhatsapp/components/whatsapp/status-ticks.tsxwhatsapp/components/whatsapp/theme-toggle.tsxwhatsapp/components/whatsapp/use-send-message.tswhatsapp/e2e/app.spec.tswhatsapp/e2e/helpers.tswhatsapp/lib/download.tswhatsapp/lib/storage.tswhatsapp/lib/store.tsx
💤 Files with no reviewable changes (1)
- whatsapp/components/whatsapp/message-bubble.tsx
🚧 Files skipped from review as they are similar to previous changes (15)
- whatsapp/components/ui/collapsible.tsx
- whatsapp/components/ui/spinner.tsx
- whatsapp/components/whatsapp/theme-toggle.tsx
- whatsapp/e2e/app.spec.ts
- whatsapp/components/whatsapp/new-chat-dialog.tsx
- whatsapp/components/whatsapp/conversation-search.tsx
- whatsapp/components/whatsapp/audio-message.tsx
- whatsapp/components/whatsapp/forward-dialog.tsx
- whatsapp/components/whatsapp/communities-view.tsx
- whatsapp/components/whatsapp/chat-list.tsx
- whatsapp/components/whatsapp/status-ticks.tsx
- whatsapp/components/ui/input-group.tsx
- whatsapp/lib/storage.ts
- whatsapp/components/whatsapp/nav-rail.tsx
- whatsapp/components/whatsapp/conversation.tsx
The re-review of b6ca71f raised one actionable point: useSendMessage captured the notifications preference at send time, so turning notifications off while a simulated reply was pending still produced a toast. It now reads the preference from a ref at fire time, matching what selectedId already did — the same class of bug, in a field I had missed. Strengthening the typing-indicator test then exposed a real hole in the earlier fix. Two problems, found in sequence: - The test injected localStorage into a live page, which raced the store's pagehide flush and got erased. It now seeds through addInitScript, so the payload lands before any app code runs. - With the injection finally working, the test failed for the right reason: `typing` was stripped on write but not on read, so a stored payload carrying the flag hydrated straight into the UI. Transient state is now sanitised in both directions. Verified: typecheck, lint, build, and the Playwright suite run twice — 38/38 across desktop and mobile both times. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011P9yhE74yqqUHnVzs6nmwM
|
O commit @coderabbitai review Generated by Claude Code |
|
✅ Action performedReview finished.
|
Resumo
Clone da interface do WhatsApp Web com toda a UI sobre o shadcn/ui, em Next.js (App Router) + Tailwind CSS v4, na pasta
whatsapp/.radix-mira)@shadcn/font-geiste@shadcn/font-heading-geist--radius: 0.375remTrês commits: a base, a migração para os primitivos oficiais de chat, e o fechamento das lacunas de comportamento com uma suíte E2E.
1. Construído sobre os primitivos oficiais de chat
O registry do shadcn expõe primitivos feitos para chat. A primeira versão tinha código escrito à mão no lugar deles; foram substituídos:
bubble(+BubbleReactions)message(MessageHeader/Content)marker(variantseparator)attachmentmessage-scrollerscrollIntoViewmanual; entrega o botão "rolar pro fim" e oscrollToMessagedo salto até a citaçãoAs fontes vêm dos itens oficiais do registry, então
--font-sanse--font-headingsão Geist.2. Funcionalidades
*negrito*,_itálico_,~tachado~,`mono`e autolink, renderizados como nós React (semdangerouslySetInnerHTML).sheet), busca na conversa e nova conversa (command), arquivadas, Chamadas, Comunidades, Configurações e QR em/connect.useReducer+ context com persistência emlocalStorage. Preferências com efeito real: confirmações de leitura governam o tique azul; notificações governam os toasts. Bloquear desabilita o compositor.Status e Canais ficaram fora por decisão de escopo.
3. Bugs encontrados e corrigidos
Uma auditoria depois da migração mostrou que a apresentação estava sólida, mas o comportamento tinha buracos.
Estado que se corrompia com o uso
[value, chat.id], gravava o texto da conversa anterior como rascunho da nova.typingera persistido: recarregar durante a simulação travava a conversa em "digitando…" para sempre.calls/communitiesfora do store: apagar uma conversa deixava chamadas órfãs que falhavam em silêncio.Componentes do registry com defeito
CommandDialogrenderizava{children}fora do contexto<Command>, então todo primitivo docmdklançavaCannot read properties of undefined (reading 'subscribe')— os dois diálogos de busca estavam inutilizáveis.spinner.tsxespalhava props de<svg>num ícone que tipastrokeWidthcomonumber.Comportamento
defaultPrevented— único sinal confiável, porque o Radix desmonta o overlay antes do listener rodar.EDIT_MESSAGEera código morto no reducer, o que tornava o rótulo "editada" inalcançável.Verificação
npm run test:e2e— 38/38 passando (19 specs × desktop e mobile), incluindo regressões para o vazamento de rascunho e o "digitando…" travado.npx tsc --noEmit✅ ·npm run lint✅ ·npm run build✅ (3 rotas estáticas)"style": "radix-mira","baseColor": "neutral",--radius: 0.375rem, Geist em corpo e headings, nenhum verde.Nota sobre determinismo: a resposta automática usa
setTimeout+Math.random(), o que deixaria a suíte intermitente.?e2e=1desliga a simulação para os testes.Como rodar
🤖 Generated with Claude Code
Summary by CodeRabbit