# Messages (Manage)

**Portal:** Manage · **Routes:** `manage.messages.*` · **Nav:** "Messages" · **Permissions:** `view-whatsapp` / `view-whatsapp-settings` / `manage-whatsapp` + `view-messenger` / `view-messenger-settings` / `manage-messenger`

The **messaging hub**: one place for every conversation the business has with a customer, across the WhatsApp Cloud API, the WhatsApp QR (Bridge) numbers and the Meta Messenger Pages. It is not a module of its own so much as the **shared information architecture** over two channel modules that keep their own engines:

- **[WhatsApp](/docs/modules_handbook/manage/messages/whatsapp/readMe.md)** — the two-provider inbox, broadcasts, flows, AI profiles, templates, tags, CTA links (`Src\Whatsapp`).
- **[Meta Messenger](/docs/modules_handbook/manage/messages/messenger/readMe.md)** — the Facebook Page inbox and its m.me attribution links (`Src\Messenger`).

> **Why a parent folder.** Before 2026-07-27 these were two sidebar groups (ten entries between them) and two inboxes. They are now one group with four entries and one inbox, so they are documented together — the conventions in [the handbook README](/docs/modules_handbook/README.md) put modules of the same area under a shared parent.

## What it does
- **One inbox.** Every conversation, whatever channel it arrived on, in a single activity-ordered list with a shared channel filter.
- **One nav entry** instead of ten. It lands on the inbox; Inbox / Broadcasts / AI Automation / Settings are **in-page hub tabs**, and each of the last three opens its own pill sub-tabs below.
- **URLs are `/manage/messages/*`.** The old `/manage/whatsapp/*` paths 301 to the new ones with the query string intact, and `GET /manage/messenger` 301s to the inbox.

## How it works

### The IA
The sidebar holds a single **Messages** link. Everything below is the in-page hub strip — main tabs as a left-aligned **underline** bar, their sub-tabs as **pills** beneath — the same shape as Funnel Marketing.

| Main tab (underline) | Lands on | Sub-tabs (pills) |
|---|---|---|
| **Dashboard** *(first tab)* | `/manage/messages/dashboard` | — (a single page: the read-across of BOTH engines — volume tiles, the in/out trend stacked by platform, per-channel traffic + connection health, replies by teammate. `Manage\Messages\DashboardController` → `Pages/Manage/Messages/Dashboard/Index.vue`; WhatsApp metrics scoped via `AccountVisibility::applyWhatsappChannels`, Messenger via `applyMessengerChannels`; Bridge-history rows (`is_historical`) excluded, and a "new chat" requires period *activity* too — a history sync creates rows with fresh `created_at` but years-old traffic. Outbound attribution = `created_by`; null groups as "Automated / AI". Same `view-whatsapp\|view-messenger` gate as the Inbox. Channel rows show the WhatsApp number under the name.) |
| **Inbox** | `/manage/messages` | — (a single page: the unified list) |
| **AI Agent** | `/manage/messages/ai-agent` | — (a single page: the messaging AI's own face — the [Zoom](/docs/modules_handbook/manage/zoom/readMe.md)/[Calls](/docs/modules_handbook/manage/call-history/readMe.md) agent-page twin on the shared `Components/AiAgent/` robot + chat. `Manage\Messages\AiAgentController` → `Pages/Manage/Messages/AiAgent/Index.vue`, chat prompt **`message_agent_chat`**. Unlike its siblings this agent already ACTS live (auto-reply/draft via `GenerateAiReply`, flows, funnel reminders, handoffs, quality guard, CTA captures) — the page is its visibility layer: outcomes split by `ai_requests.meta.mode` (auto vs draft), capability cards over the real machinery, a work diary from the ledgers, and a next-steps checklist — reconnect dead channels, review waiting `ai_draft`s, assign profiles to brainless channels, inspect paused broadcasts.) |
| **Broadcasts** | `/manage/messages/broadcasts` | Campaigns · Templates · Segments · Settings |
| **AI Automation** | `/manage/messages/cta-links` | CTA Links · AI Setting · Flows · Sandbox |
| **Settings** | `/manage/messages/channels` | Channels · Tags · General · **Delivery APIs** (`/manage/integrations/messaging`, behind `view-integrations` — the DB-backed SMS/SMTP/GetResponse credentials, see [Messaging credentials](/docs/modules_handbook/shared/messaging-credentials/readMe.md)) |

- **[`Components/Messages/MessagesTabs.vue`](/resources/js/Components/Messages/MessagesTabs.vue)** is the config; presentation comes from the shared **[`HubTabs`](/resources/js/Components/HubTabs.vue)** (underline mains, pill subs), so every page mounts one line: `<MessagesTabs />`. Tabs the user lacks permission for are dropped, so a settings-only admin never sees one that would 403.
  - **Inbox is matched last.** Its prefix `/manage/messages` is a prefix of every other tab's path, so `matches()` claims them all; `activeMain` prefers any non-Inbox hit and falls back to Inbox. Get this wrong and the whole module reads as "Inbox".
  - Single-level sections elsewhere in Manage (Products, System, Zoom, Lead Distribution) use the flatter [`Components/SectionTabs.vue`](/resources/js/Components/SectionTabs.vue) instead — Messages is the only two-level hub here.
- **Nav highlighting.** The single sidebar entry carries `prefixes: ['/manage/messages']`, so it stays lit anywhere in the module. (`AppShell.vue`'s `childActive()` / `prefixes` support remains — other groups still use it.)
- **Where things moved.** Channels and Tags became Settings tabs; Templates became a Broadcasts tab; CTA Links / AI Setting / Flows / Sandbox became the AI Automation tabs. The **AI auto-reply guards** moved off Settings onto **AI Setting** (they still `PUT` `manage.messages.settings.update`) — see [settings.md](/docs/modules_handbook/manage/messages/whatsapp/settings.md). **Tags are now created in the inbox** (an *Add tag* button beside the picker); the Settings tab is for renaming, recolouring and deleting — see [tags.md](/docs/modules_handbook/manage/messages/whatsapp/tags.md).

### The unified inbox
`Manage\Whatsapp\InboxController` renders both platforms. Each side fetches its own top window (`perPage + 1`), they are merged by `last_activity_at` and re-windowed, so the combined first N is the true newest N however the traffic splits — and it stays correct as the infinite scroll grows `perPage`.

- **`platform` is the discriminator.** Every conversation row and channel option carries `'whatsapp'` or `'messenger'`. The frontend branches on it for the endpoint root, the Echo channel and which controls to offer — never on the absence of a key.
- **[`Src\Messenger\Support\InboxPresenter`](/src/Messenger/Support/InboxPresenter.php)** maps a Messenger conversation / message into the WhatsApp inbox shape, returning WhatsApp-only concepts as their empty value (`tags: []`, `is_group: false`, `ai_draft: null`) so nothing needs a guard just to *read* a row. It also translates the message type — the two `TYPE_*` enums agree up to `STICKER` and diverge after it. It **mirrors `InboxController::transformConversation()`; when a key is added there, add it here too.**
- **Only the read side is merged.** Sends, webhooks, tables, Echo channels (`whatsapp.*` vs `messenger.*`, with their own event names) and permissions stay per-platform.
- **Honest degradation, not silent no-ops.** WhatsApp-only affordances (tags, flows, AI drafts / handoff, templates, groups, voice notes, reactions, quote-reply, click-to-call) are **hidden** on a Messenger thread. Filters that are WhatsApp concepts (tag, needs-review, unreplied, groups) **exclude** Messenger rows rather than appearing to apply. Bulk mark-read covers both platforms; bulk tag is WhatsApp-only because tags live on WhatsApp contacts. A closed 24h Messenger window **locks the composer** with an explanation instead of letting an agent type a reply Meta will reject.
- **Permissions.** The inbox route accepts `view-whatsapp` **or** `view-messenger`; each platform's payload is scoped independently, so a Messenger-only admin sees only Pages and a WhatsApp-only admin sees no Messenger rows.

## Related files
**Controllers** — [`app/Http/Controllers/Manage/Whatsapp/InboxController.php`](/app/Http/Controllers/Manage/Whatsapp/InboxController.php) (the merge: `messengerConversations()`, `openMessengerThread()`, `messengerChannelOptions()`) · `Manage/Messenger/InboxController.php` (thread actions only) · `Manage/Whatsapp/CtaLinksController.php` (lists both platforms' CTA links)
**Presenter** — [`src/Messenger/Support/InboxPresenter.php`](/src/Messenger/Support/InboxPresenter.php)
**Frontend** — [`resources/js/Pages/Manage/Messages/`](/resources/js/Pages/Manage/Messages/) (every page; moved from `Pages/Manage/Whatsapp/`) · [`Components/Messages/MessagesTabs.vue`](/resources/js/Components/Messages/MessagesTabs.vue) + [`Components/HubTabs.vue`](/resources/js/Components/HubTabs.vue) · [`Components/Messages/TagFormModal.vue`](/resources/js/Components/Messages/TagFormModal.vue) (shared by the Tags page and the inbox) · [`Layouts/ManageLayout.vue`](/resources/js/Layouts/ManageLayout.vue) (`messagesGroup`) · [`Layouts/AppShell.vue`](/resources/js/Layouts/AppShell.vue) (`childActive()` / `prefixes`)
**Routes** — [`routes/web.php`](/routes/web.php) — the `manage.messages.*` group, the `manage.whatsapp.legacy` catch-all redirect, and the `manage.messenger.*` thread + CTA write endpoints.
**Image gallery** — clicking any picture in a thread browses every image in it, via the shared [`ImageLightbox`](/docs/modules_handbook/shared/image-lightbox/readMe.md) + `useMessageGallery` (`Components/Conversation/MessageAttachment.vue`'s opt-in `gallery` prop is the trigger). That doc is the canonical one — this inbox is its *Reference usage* example.

**See also:** [WhatsApp](/docs/modules_handbook/manage/messages/whatsapp/readMe.md) · [Meta Messenger](/docs/modules_handbook/manage/messages/messenger/readMe.md) · [Leads](/docs/modules_handbook/manage/leads/readMe.md) (the Channel tabs that link back into this inbox) · [Meta Ads · Connection](/docs/modules_handbook/manage/meta-ads/connection/readMe.md) (where a Page is connected before Messenger can use it).
