Skip to content

Clone do WhatsApp construído sobre shadcn/ui (Mira · Neutro · Geist · radius Small) - #2

Merged
matheusfrainer merged 5 commits into
mainfrom
claude/whatsapp-shadcn-app-69b77m
Aug 6, 2026
Merged

Clone do WhatsApp construído sobre shadcn/ui (Mira · Neutro · Geist · radius Small)#2
matheusfrainer merged 5 commits into
mainfrom
claude/whatsapp-shadcn-app-69b77m

Conversation

@matheusfrainer

@matheusfrainer matheusfrainer commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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/.

Item Valor
Style Mira (radix-mira)
Base de cor / tema Neutro — sem verde, 100% neutro
Fonte (body + heading) Geist, via @shadcn/font-geist e @shadcn/font-heading-geist
Radius Small--radius: 0.375rem
Biblioteca base / ícones Radix UI / Hugeicons

Trê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:

Primitivo Substitui
bubble (+ BubbleReactions) markup manual do balão; entrega as reações
message (MessageHeader/Content) autor e estrutura da linha
marker (variant separator) separadores de data, aviso de criptografia, mensagens de sistema, divisor de não lidas
attachment documentos e mídia, com estados
message-scroller scrollIntoView manual; entrega o botão "rolar pro fim" e o scrollToMessage do salto até a citação

As fontes vêm dos itens oficiais do registry, então --font-sans e --font-heading são Geist.

2. Funcionalidades

  • Mensagens — responder/citar com salto até a original, reações, encaminhar, editar, favoritar, fixar, copiar, apagar e seleção múltipla com barra de ações.
  • Formatação*negrito*, _itálico_, ~tachado~, `mono` e autolink, renderizados como nós React (sem dangerouslySetInnerHTML).
  • Mídia — imagem com visualizador, vídeo, voz com waveform/scrub/velocidade, documento, localização e contato. As "fotos" são SVG data URI geradas localmente: o app não faz nenhuma requisição de rede.
  • Chamadas simuladas — chamando → conectada → encerrada, com cronômetro, mudo/câmera/viva-voz, e registro no histórico com a duração real.
  • Navegação — rail, painel do contato/grupo (sheet), busca na conversa e nova conversa (command), arquivadas, Chamadas, Comunidades, Configurações e QR em /connect.
  • EstadouseReducer + context com persistência em localStorage. 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

  • Rascunho quebrado em três frentes, uma causa: o compositor não era derivado da conversa — nunca carregava o rascunho, levava o texto digitado para a conversa seguinte e, porque o debounce dependia de [value, chat.id], gravava o texto da conversa anterior como rascunho da nova.
  • typing era persistido: recarregar durante a simulação travava a conversa em "digitando…" para sempre.
  • Ids de encaminhamento derivados da origem, colidindo ao encaminhar a mesma mensagem duas vezes para a mesma conversa.
  • calls/communities fora do store: apagar uma conversa deixava chamadas órfãs que falhavam em silêncio.

Componentes do registry com defeito

  • CommandDialog renderizava {children} fora do contexto <Command>, então todo primitivo do cmdk lançava Cannot read properties of undefined (reading 'subscribe') — os dois diálogos de busca estavam inutilizáveis.
  • spinner.tsx espalhava props de <svg> num ícone que tipa strokeWidth como number.

Comportamento

  • Escape fechava a conversa ao dispensar um sheet/diálogo. O handler global agora sai em defaultPrevented — único sinal confiável, porque o Radix desmonta o overlay antes do listener rodar.
  • Pílula de reação colidia com a mensagem seguinte.
  • EDIT_MESSAGE era código morto no reducer, o que tornava o rótulo "editada" inalcançável.
  • Busca da lista cobria só a última mensagem; agora cobre a transcrição inteira.
  • Downloads viraram arquivos de verdade: imagens salvam o data URI e documentos geram um PDF com tabela xref válida.

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)
  • Especificação original preservada: "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=1 desliga a simulação para os testes.

Como rodar

cd whatsapp
npm install
npm run dev        # http://localhost:3000
npm run test:e2e   # suíte end-to-end

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a complete WhatsApp Web-style experience with chats, messaging, calls, communities, settings, search, media sharing, reactions, replies, forwarding, and contact management.
    • Added Portuguese connection instructions with QR pairing display.
    • Added light and dark themes, responsive layouts, local persistence, simulated conversations and calls, audio messages, attachments, rich-text formatting, notifications, and message status indicators.
  • Documentation
    • Added Portuguese project documentation covering features, setup, architecture, and limitations.
  • Tests
    • Added end-to-end coverage for messaging, navigation, calls, media, persistence, and contact actions.

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
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b911e764-8612-43a2-bdf2-b8d5e158ae2a

📥 Commits

Reviewing files that changed from the base of the PR and between b6ca71f and 519d441.

📒 Files selected for processing (3)
  • whatsapp/components/whatsapp/use-send-message.ts
  • whatsapp/e2e/app.spec.ts
  • whatsapp/lib/storage.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • whatsapp/e2e/app.spec.ts
  • whatsapp/lib/storage.ts
  • whatsapp/components/whatsapp/use-send-message.ts

📝 Walkthrough

Walkthrough

Added 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.

Changes

WhatsApp Web clone

Layer / File(s) Summary
Project setup and application foundation
README.md, whatsapp/*.json, whatsapp/*.config.*, whatsapp/app/*, whatsapp/components/theme-provider.tsx
Added project documentation, configuration, formatting rules, the root layout, global theme styles, theme switching, and the connection route.
Domain model, seeded data, and persistence
whatsapp/lib/types.ts, whatsapp/lib/data.ts, whatsapp/lib/format.ts, whatsapp/lib/media.ts, whatsapp/lib/storage.ts, whatsapp/lib/store.tsx
Added WhatsApp domain types, seeded chats, calls, communities, rich-text and media helpers, local storage validation, persistence sanitization, and reducer-based state management.
Reusable UI primitives
whatsapp/components/ui/*, whatsapp/lib/utils.ts
Added Radix-based overlays and menus, CVA variants, command and input controls, message and attachment primitives, scrolling, feedback, and form components.
WhatsApp application features
whatsapp/components/whatsapp/*, whatsapp/app/page.tsx
Added navigation, chat lists, conversations, message composition and actions, media rendering, calls, communities, settings, contact details, responsive behavior, and application providers.
End-to-end validation
whatsapp/e2e/*, whatsapp/playwright.config.ts
Added deterministic Playwright helpers and tests for persistence, messaging, calls, blocking, navigation, deletion, read receipts, and attachments.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve claramente o clone do WhatsApp e as principais tecnologias e escolhas visuais usadas na implementação.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/whatsapp-shadcn-app-69b77m

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

claude added 2 commits August 5, 2026 03:47
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
@matheusfrainer
matheusfrainer marked this pull request as ready for review August 5, 2026 17:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (11)
whatsapp/components/whatsapp/use-send-message.ts (1)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move 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 nextId and hides @/lib/id as the owner. Update the callers that import nextId from 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 win

Validate each chat entry, not just the array.

The check accepts any non-empty array. A stored entry without conversation or id then reaches the renderer and throws, and the seed fallback never runs. Validate the required fields of each chat and return null when 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 win

Move 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: hydrated stays true, so the persist effect at Lines 378-394 immediately writes the seed back under the same key. Dispatch RESET from a handler that calls clearStored() 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 win

Debounce persistence, because drafts write on every keystroke.

SET_DRAFT replaces the chat object, so state.chats changes for each typed character. This effect then serializes all chats and writes to localStorage synchronously on the typing path. The cost grows with conversation size. Debounce the write, or exclude draft from 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 win

Use next/link for the /connect navigation.

/connect is an internal App Router route, so replace the plain <a> with Link to 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 value

Remove the unused isGroup prop.

MessageActionsProps declares isGroup, 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 win

Expose 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 win

Move setPlaying(false) out of the setElapsed updater.

setElapsed receives a pure updater function. Calling setPlaying(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 win

Add focus management to the call overlay.

The overlay declares role="dialog" and aria-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 Radix Dialog primitive 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 win

Use the shared Input primitive for the search field.

This field is a raw <input> with inline utility classes. The project provides Input in whatsapp/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 win

Announce the unread count on the chats control.

The badge conveys the unread count visually only. aria-label stays "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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca980e and 51e7494.

⛔ Files ignored due to path filters (2)
  • whatsapp/app/favicon.ico is excluded by !**/*.ico
  • whatsapp/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (89)
  • README.md
  • whatsapp/.gitignore
  • whatsapp/.prettierignore
  • whatsapp/.prettierrc
  • whatsapp/AGENTS.md
  • whatsapp/README.md
  • whatsapp/app/connect/page.tsx
  • whatsapp/app/globals.css
  • whatsapp/app/layout.tsx
  • whatsapp/app/page.tsx
  • whatsapp/components.json
  • whatsapp/components/.gitkeep
  • whatsapp/components/theme-provider.tsx
  • whatsapp/components/ui/attachment.tsx
  • whatsapp/components/ui/avatar.tsx
  • whatsapp/components/ui/badge.tsx
  • whatsapp/components/ui/bubble.tsx
  • whatsapp/components/ui/button.tsx
  • whatsapp/components/ui/collapsible.tsx
  • whatsapp/components/ui/command.tsx
  • whatsapp/components/ui/context-menu.tsx
  • whatsapp/components/ui/dialog.tsx
  • whatsapp/components/ui/dropdown-menu.tsx
  • whatsapp/components/ui/empty.tsx
  • whatsapp/components/ui/input-group.tsx
  • whatsapp/components/ui/input.tsx
  • whatsapp/components/ui/item.tsx
  • whatsapp/components/ui/marker.tsx
  • whatsapp/components/ui/message-scroller.tsx
  • whatsapp/components/ui/message.tsx
  • whatsapp/components/ui/popover.tsx
  • whatsapp/components/ui/separator.tsx
  • whatsapp/components/ui/sheet.tsx
  • whatsapp/components/ui/skeleton.tsx
  • whatsapp/components/ui/slider.tsx
  • whatsapp/components/ui/sonner.tsx
  • whatsapp/components/ui/spinner.tsx
  • whatsapp/components/ui/switch.tsx
  • whatsapp/components/ui/textarea.tsx
  • whatsapp/components/ui/tooltip.tsx
  • whatsapp/components/whatsapp/audio-message.tsx
  • whatsapp/components/whatsapp/call-overlay.tsx
  • whatsapp/components/whatsapp/calls-view.tsx
  • whatsapp/components/whatsapp/chat-composer.tsx
  • whatsapp/components/whatsapp/chat-list-item.tsx
  • whatsapp/components/whatsapp/chat-list.tsx
  • whatsapp/components/whatsapp/communities-view.tsx
  • whatsapp/components/whatsapp/contact-panel.tsx
  • whatsapp/components/whatsapp/conversation-search.tsx
  • whatsapp/components/whatsapp/conversation.tsx
  • whatsapp/components/whatsapp/emoji-picker.tsx
  • whatsapp/components/whatsapp/empty-conversation.tsx
  • whatsapp/components/whatsapp/forward-dialog.tsx
  • whatsapp/components/whatsapp/icon.tsx
  • whatsapp/components/whatsapp/icons.ts
  • whatsapp/components/whatsapp/media-message.tsx
  • whatsapp/components/whatsapp/message-actions.tsx
  • whatsapp/components/whatsapp/message-bubble.tsx
  • whatsapp/components/whatsapp/nav-rail.tsx
  • whatsapp/components/whatsapp/new-chat-dialog.tsx
  • whatsapp/components/whatsapp/rich-text.tsx
  • whatsapp/components/whatsapp/settings-view.tsx
  • whatsapp/components/whatsapp/status-ticks.tsx
  • whatsapp/components/whatsapp/theme-toggle.tsx
  • whatsapp/components/whatsapp/use-send-message.ts
  • whatsapp/components/whatsapp/whatsapp-app.tsx
  • whatsapp/e2e/app.spec.ts
  • whatsapp/e2e/helpers.ts
  • whatsapp/e2e/messaging.spec.ts
  • whatsapp/eslint.config.mjs
  • whatsapp/hooks/.gitkeep
  • whatsapp/hooks/use-hydrated.ts
  • whatsapp/lib/.gitkeep
  • whatsapp/lib/data.ts
  • whatsapp/lib/download.ts
  • whatsapp/lib/e2e.ts
  • whatsapp/lib/format.ts
  • whatsapp/lib/id.ts
  • whatsapp/lib/media.ts
  • whatsapp/lib/storage.ts
  • whatsapp/lib/store.tsx
  • whatsapp/lib/types.ts
  • whatsapp/lib/utils.ts
  • whatsapp/next.config.ts
  • whatsapp/package.json
  • whatsapp/playwright.config.ts
  • whatsapp/postcss.config.mjs
  • whatsapp/public/.gitkeep
  • whatsapp/tsconfig.json

Comment thread whatsapp/components/ui/collapsible.tsx
Comment thread whatsapp/components/ui/input-group.tsx
Comment thread whatsapp/components/ui/switch.tsx Outdated
Comment thread whatsapp/components/whatsapp/call-overlay.tsx
Comment thread whatsapp/components/whatsapp/communities-view.tsx
Comment thread whatsapp/components/whatsapp/status-ticks.tsx
Comment thread whatsapp/components/whatsapp/theme-toggle.tsx Outdated
Comment thread whatsapp/components/whatsapp/use-send-message.ts Outdated
Comment thread whatsapp/e2e/app.spec.ts
Comment thread whatsapp/lib/download.ts
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
@matheusfrainer

Copy link
Copy Markdown
Owner Author

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 b6ca71f.

Confirmados e corrigidos

Estes eu validei empiricamente antes de mexer:

  • switch.tsx usava data-checked/data-unchecked — conferi o pacote Radix instalado: ele emite data-state. O componente veio assim do registry, então a cor da trilha e a posição do thumb nunca mudavam. Corrigido para data-[state=*] e verificado no navegador que o background-color agora realmente alterna.
  • Escape durante a chamada fechava a conversa atrás — o Shell registra o listener antes (é filho, efeito roda primeiro), então observava o evento antes do preventDefault. Passou para a fase de captura.
  • Timers da resposta simulada morriam ao trocar de conversa — estavam presos ao compositor, que tem key={chat.id}. Consequência: o toast de outra conversa era inalcançável. Movidos para escopo de módulo, com flush em pagehide.
  • buildPdf calculava /Length e offsets do xref com String#length enquanto o Blob codifica UTF-8 — qualquer nome acentuado dessincronizava e gerava PDF inválido. Payload dobrado para ASCII (o que também combina com Type1/Helvetica) e conferido byte a byte com entrada acentuada.
  • "Favoritar" em lote alternava cada mensagem, desfavoritando parte de uma seleção mista. STAR_MESSAGES agora aceita alvo explícito.
  • clearStored() dentro do reducer → movido para o call site.
  • Chevron do painel de contato não girava (data-state fica no trigger).
  • Ícone errado no item "Arquivar conversa".
  • Rejeição do writeText descartada → agora reporta sucesso/falha.
  • setPlaying dentro do updater de setElapsed.
  • Valores de CommandItem podiam colidir → prefixados com o id.
  • Estado persistido aceito só pela forma do array → cada conversa é validada.

O comentário sobre o teste de typing estava certo e foi o mais útil: ele passava sem exercitar a regressão. Agora semeia localStorage com typing: true e verifica que a hidratação limpa o flag na UI e no storage.

Uma correção que gerou regressão, pega pela suíte

Apliquei 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 pagehide — ganha a performance sem custar durabilidade.

Não aplicado

Cobertura de docstrings (6,25% vs 80%). Este código comenta decisões — por que o Escape usa defaultPrevented, por que os offsets do PDF são bytes — em vez de repetir assinaturas. Encher cada função para bater uma razão adicionaria ruído, não clareza. Se o limiar for requisito do repositório, me diga e eu reconsidero.

Sobre a sugestão de gerar testes unitários: seria redundante com a suíte E2E de 38 testes já versionada em e2e/.

Verificação

tsc --noEmit ✅ · lint ✅ · build ✅ · npm run test:e2e 38/38 em desktop e mobile.


Generated by Claude Code

@matheusfrainer

Copy link
Copy Markdown
Owner Author

A review do b6ca71f não chegou a rodar — bateu no limite de uso e o status ficou Review rate limited. O limite já resetou.

Uma nota sobre a única thread que não foi marcada como resolvida, em contact-panel.tsx (vídeo passado para <img> na grade de mídia): está corrigida, mas de forma diferente da sugerida. Em vez de adicionar markup de vídeo ou um campo de thumbnail, excluí vídeo da grade — nenhuma mensagem de vídeo é criada em lugar nenhum do app (o compositor não envia e o seed não contém), então renderizar um player para um caso inexistente seria código morto. Se o tipo video passar a ser realmente produzido, aí o tratamento adequado entra junto.

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@matheusfrainer Entendido. A exclusão de mensagens de vídeo da grade remove o caso de <img> inválido no estado atual. Vou considerar essa thread como não aplicável.

Vou executar uma nova review do PR.


✏️ Learnings added
Learnt from: matheusfrainer
URL: https://github.com/matheusfrainer/personal/pull/2

Timestamp: 2026-08-05T18:41:47.519Z
Learning: In `whatsapp/components/whatsapp/contact-panel.tsx`, the contact-panel media grid intentionally excludes `video` messages. The current application does not produce video messages: neither the composer nor the seed data creates them. Add video-grid rendering only when the application produces video messages, with the required thumbnail or video markup.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Read the current notification preference in the delayed callback.

notify is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 51e7494 and b6ca71f.

📒 Files selected for processing (26)
  • whatsapp/components/ui/collapsible.tsx
  • whatsapp/components/ui/input-group.tsx
  • whatsapp/components/ui/sonner.tsx
  • whatsapp/components/ui/spinner.tsx
  • whatsapp/components/ui/switch.tsx
  • whatsapp/components/whatsapp/audio-message.tsx
  • whatsapp/components/whatsapp/call-overlay.tsx
  • whatsapp/components/whatsapp/chat-list.tsx
  • whatsapp/components/whatsapp/communities-view.tsx
  • whatsapp/components/whatsapp/contact-panel.tsx
  • whatsapp/components/whatsapp/conversation-search.tsx
  • whatsapp/components/whatsapp/conversation.tsx
  • whatsapp/components/whatsapp/forward-dialog.tsx
  • whatsapp/components/whatsapp/message-actions.tsx
  • whatsapp/components/whatsapp/message-bubble.tsx
  • whatsapp/components/whatsapp/nav-rail.tsx
  • whatsapp/components/whatsapp/new-chat-dialog.tsx
  • whatsapp/components/whatsapp/settings-view.tsx
  • whatsapp/components/whatsapp/status-ticks.tsx
  • whatsapp/components/whatsapp/theme-toggle.tsx
  • whatsapp/components/whatsapp/use-send-message.ts
  • whatsapp/e2e/app.spec.ts
  • whatsapp/e2e/helpers.ts
  • whatsapp/lib/download.ts
  • whatsapp/lib/storage.ts
  • whatsapp/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
@matheusfrainer

Copy link
Copy Markdown
Owner Author

O commit 519d441 continua sem review: a tentativa das 18:56 bateu no limite de uso e o CodeRabbit não repete a tentativa sozinho. O limite já resetou.

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

@matheusfrainer Vou executar uma nova review do PR. Ela incluirá o commit 519d441.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@matheusfrainer
matheusfrainer merged commit c30ebe9 into main Aug 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants