DocHub
Photo purge across all contacts, view-once message alerts, media browser navigation, UI polish

WANK 4.0 — Purge Everywhere, View-Once Detection & UX Refinements

Built on top of WANK 3 (documented in WANK-3.0.md). This covers the photo purge-everywhere system for removing identical photos across all contacts, view-once message detection with CRM alerts, media browser navigation improvements, and UI polish.


Table of Contents

  1. What Changed Since WANK 3
  2. Purge Identical Photos Everywhere
  3. View-Once Message Detection
  4. Media Browser — Chat Navigation
  5. Avatar Lightbox Sizing
  6. Photo Grid Button Visibility
  7. Files Modified
  8. Key Lessons & Gotchas

What Changed Since WANK 3

Category What Was Added
Purge Everywhere Select any photo in the media browser, find all identical copies by MD5 hash across every contact folder, and replace them all with grey “Removed” placeholders
View-Once Detection Incoming view-once photos/videos are detected and shown as an orange alert in the conversation instead of attempting a media download
Media → Chat Nav Clicking a contact in the media browser sidebar returns to their conversation instead of loading their media
Avatar Lightbox Enlarged minimum size so small WhatsApp avatars don’t appear tiny in the lightbox
Button Visibility Photo grid hover buttons (purge/download/delete) made bright and high-contrast

Purge Identical Photos Everywhere

Problem

The user sends photos of themselves to many contacts. These identical files accumulate across all contact folders, wasting disk space and leaving personal photos scattered everywhere.

Solution

A two-step flow: scan then replace.

Backend — MediaService

Four new methods:

Method Purpose
getPlaceholder(size) Generates a grey 200x200 PNG with “Removed” text via SVG overlay through sharp. Result cached in memory.
getPlaceholderJpeg(size, quality) Same but JPEG output, used for thumbs/ and micro/ directories. Also cached.
findIdenticalFiles(folder, filename) Reads source file, computes MD5. Scans ALL contact folders (photos/ subdirs + flat-layout roots). Uses stat.size pre-check to skip non-matching files without reading them. Returns { hash, size, matches[] }. Guards against re-scanning already-purged files by checking the placeholder’s own MD5.
purgeIdenticalEverywhere(folder, filename) Calls findIdenticalFiles(), then overwrites each match’s photo with the PNG placeholder, its thumbnail with a JPEG placeholder, and its micro-thumbnail with a smaller JPEG placeholder. Returns { replaced, affectedFolders[] }.

Size pre-check optimization: Before computing an MD5 hash (which requires reading the entire file), findIdenticalFiles compares stat.size against the source. Since most files differ in size, this skips the vast majority without any I/O beyond a stat call.

Placeholder caching: The PNG, thumb JPEG, and micro JPEG placeholders are generated once and held in memory for the lifetime of the process. The placeholder’s own MD5 is also cached to prevent re-purging files that are already placeholders.

Backend — Controller & Routes

Route Method Handler Purpose
/api/media/purge-preview/:folder/:filename GET purgePreview Scan for matches, return count + list for confirmation
/api/media/purge-everywhere/:folder/:filename POST purgeEverywhere Execute replacement, return affected count

Frontend — API

Two new mediaAPI methods:

purgePreview(folder, filename)   → { hash, size, matchCount, folderCount, matches[] }
purgeEverywhere(folder, filename) → { replaced, affectedFolders[] }

Frontend — MediaBrowserPage UI

Three UI additions:

  1. Photo grid hover button — Orange Ban icon, positioned left of download/delete buttons. Appears on hover over any photo thumbnail.

  2. Lightbox top bar button — Same orange Ban icon in the full-screen photo viewer, next to download and delete.

  3. Confirmation modal (z-60, above lightbox z-50):

    • Loading spinner while scanning all folders
    • Match count, affected folder count, file size, truncated MD5
    • Scrollable list of every match (folder + filename)
    • Warning that replacement is irreversible
    • Cancel / “Replace N files” buttons
    • After confirm: replaces files, closes lightbox, refreshes folder view

View-Once Message Detection

Problem

WhatsApp “view once” photos and videos can only be opened on the phone. The CRM has no way to know one was received — the user might miss it entirely.

Solution

Detect msg._data.isViewOnce on incoming messages and surface a visible alert in the conversation.

Backend — WhatsAppService

In handleMessage():

  • Checks msg._data.isViewOnce before any media download attempt
  • If true: logs [View-once photo — check your phone] to the contact’s transcript file
  • Sets hasMedia: false in the serialized message (no broken media placeholder)
  • Sets isViewOnce: true in the serialized message
  • Sets the body to [View-once photo — check your phone] if body is empty
  • Returns early — skips downloadMedia() entirely (it would fail or be empty anyway)
  • Still emits the WebSocket event so the frontend updates in real-time

In serializeMessage():

  • Reads msg._data.isViewOnce and includes it in the serialized object

In archiveMessage():

  • Stores is_view_once boolean in the messages table

Database

New column on the messages table:

ALTER TABLE messages ADD COLUMN IF NOT EXISTS is_view_once BOOLEAN DEFAULT FALSE;

Applied to local DB and all production slice databases.

Backend — messageController

The message query row mapper now includes:

isViewOnce: r.is_view_once || false

Frontend — Types

export interface Message {
  // ...existing fields...
  isViewOnce?: boolean;
}

Frontend — MessageBubble

When message.isViewOnce is true, an orange banner renders above the message body:

  • EyeOff icon from lucide-react
  • Text: “View-once — check your phone”
  • Styled with bg-orange-500/10 background and border-orange-500/20 border
  • Appears in the same position as the “Imported” and “AI” badges

Media Browser — Chat Navigation

Before

Clicking a contact in the media browser’s left sidebar loaded their media files in the right panel. To return to the conversation, the user had to manually navigate back to the chat page and find the contact again.

After

Clicking a contact in the sidebar now:

  1. Calls selectChat(chat) from the WhatsApp context (loads messages)
  2. Navigates to / (the chat page)

The user lands directly in the conversation with that contact. To browse media, they can use the “Browse media” button on any photo in the conversation, or navigate to /media via the nav bar.

Implementation: Added useNavigate from react-router-dom and selectChat from the WhatsApp context to MediaBrowserPage. The handleSelectChat handler was simplified from a multi-state update to a two-line function.


Avatar Lightbox Sizing

Problem

WhatsApp profile pictures are typically low-resolution (96x96 to 200x200 pixels). When clicked in the CRM panel, the lightbox rendered the image at its natural size — appearing tiny in the center of a dark overlay.

Fix

Added min-w-[300px] min-h-[300px] to the lightbox <img> element in CrmPanel.tsx. The image now displays at least 300x300 pixels. Higher-resolution images still scale naturally up to the max-w-full max-h-[80vh] constraints.


Photo Grid Button Visibility

Problem

The three hover action buttons on photo thumbnails (purge/download/delete) used muted colors (text-orange-400/70, text-white/70, text-red-400/70) with semi-transparent backgrounds (bg-black/50). On busy photo thumbnails, they were hard to see.

Fix

All three buttons updated to:

  • Full-opacity icon colors: text-orange-400, text-blue-400, text-red-400
  • Darker background: bg-black/80
  • Larger icons: w-4 h-4 (up from w-3.5 h-3.5)
  • Larger hit target: p-1.5 (up from p-1)
  • Drop shadow: drop-shadow-lg
  • Hover state: Background fills with the icon’s color, text goes white (e.g., hover:bg-orange-500 hover:text-white)
Button Default Color Hover Color
Remove everywhere Orange icon on dark bg Solid orange bg, white icon
Download Blue icon on dark bg Solid blue bg, white icon
Delete Red icon on dark bg Solid red bg, white icon

Files Modified

File Changes
backend/src/services/MediaService.ts getPlaceholder(), getPlaceholderJpeg(), findIdenticalFiles(), purgeIdenticalEverywhere() + placeholder cache fields
backend/src/services/WhatsAppService.ts View-once detection in handleMessage(), isViewOnce in serializeMessage(), is_view_once in archiveMessage()
backend/src/controllers/mediaController.ts purgePreview, purgeEverywhere controllers
backend/src/controllers/messageController.ts isViewOnce field in message row mapper
backend/src/routes/mediaRoutes.ts Two new routes: GET purge-preview, POST purge-everywhere
frontend/src/services/api.ts mediaAPI.purgePreview(), mediaAPI.purgeEverywhere()
frontend/src/types/index.ts isViewOnce on Message interface
frontend/src/pages/MediaBrowserPage.tsx Purge button (grid + lightbox), confirmation modal, chat navigation on sidebar click, brighter hover buttons
frontend/src/components/MessageBubble.tsx EyeOff import, view-once orange banner
frontend/src/components/CrmPanel.tsx Avatar lightbox min-w-[300px] min-h-[300px]

Key Lessons & Gotchas

1. View-Once Property Is on _data, Not the Message Object

whatsapp-web.js does not expose isViewOnce as a top-level Message property. It exists on msg._data.isViewOnce. The library uses it when sending view-once messages (as a sendMessage option), but for received messages it’s only on the raw data object.

2. View-Once Media Download Is Pointless

View-once photos can only be opened on the phone. Even if downloadMedia() succeeds on WhatsApp Web, the content may be empty or a placeholder. The correct approach is to skip the download entirely and alert the user to check their phone.

3. Placeholder Hash Guard Prevents Re-Purging

Without checking whether a file is already a placeholder, running purge-everywhere on a previously purged photo would find all the placeholders (which are now identical to each other) and “purge” them again — an expensive no-op. Caching the placeholder’s MD5 and checking it first makes the operation idempotent.

4. stat.size Pre-Check Is Essential for Performance

Computing MD5 requires reading the entire file. For a media folder with thousands of photos, hashing every file would be extremely slow. Comparing stat.size first (a single syscall) eliminates >99% of candidates immediately since most photos differ in byte size.

5. Context selectChat vs URL Navigation

The chat page doesn’t read a query parameter to select a chat — the active chat is held in React context (WhatsAppContext.selectChat). Navigating to /?chat=xxx doesn’t work. The correct pattern is to call selectChat(chat) first (which loads messages and sets context state), then navigate('/').