- Create Liquid i18n filter (LiquidFilters::I18nFilter) exposing I18n.t() as `t` filter
- Replace hardcoded English strings in all 26 Liquid email templates with i18n keys
- Replace hardcoded English strings in all 4 Devise ERB email templates with t() calls
- Translate all mailer subjects to use I18n.t() for locale-aware rendering
- Add comprehensive pt-BR translations for all transactional emails
- Fix ApplicationRecord#to_drop to walk STI hierarchy (SuperAdmin -> User)
* fix(whatsapp): use payload filename for documents to avoid Content-Disposition parsing issues
When receiving documents via the WhatsApp Cloud API, filenames with
spaces or special characters were mangled due to the Down gem's
case-sensitive Content-Disposition header parsing. Now uses the
filename from the WhatsApp message payload when available, falling
back to Content-Disposition for other attachment types.
* fix: scope payload filename preference to document messages only
* fix: revert document-only scope, apply payload filename for all attachment types
Previously, all incoming messages from Facebook channel with
instagram_id had their attachment data_url and thumb_url overridden with
external_url. This caused issues for non-Instagram conversations
originating from Facebook Message where the file URL should be used
instead.
Narrows the override to only apply when the conversation type is
instagram_direct_message, which is the only case where Instagram's CDN
URLs need to be used directly.
Fixes
https://linear.app/chatwoot/issue/CW-6722/videos-are-missing-in-facebook-conversation
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an AgentBot is assigned to a conversation after the first message
has already been received, the bot does not respond because it never
receives any event. The `message_created` event fires before the bot is
assigned, and the bot has no way to know it was assigned.
Chatwoot already dispatches a `CONVERSATION_UPDATED` event when
`assignee_agent_bot_id` changes, but `AgentBotListener` wasn't listening
for it. This fix adds a `conversation_updated` handler so the bot
receives a webhook with the conversation context when assigned.
## How to reproduce
1. Customer sends a message → conversation created, `message_created`
fires
2. System processes the message (adds labels, custom attributes)
3. System assigns an AgentBot to the conversation via API
4. **Before fix:** Bot receives no event and never responds
5. **After fix:** Bot receives `conversation_updated` event with
conversation payload
## What changed
- **`AgentBotListener`**: Added `conversation_updated` handler that
sends the conversation webhook payload to the assigned bot when the
conversation is updated
## How to test
1. Create an AgentBot with an `outgoing_url` pointing to a webhook
inspector (e.g. webhook.site)
2. Send a message to create a conversation
3. Assign the AgentBot to the conversation via API:
```
POST /api/v1/accounts/{id}/conversations/{id}/assignments
{ "assignee_id": <bot_id>, "assignee_type": "AgentBot" }
```
4. Verify the bot receives a `conversation_updated` event at its webhook
URL
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### Description
When integrating the web widget via the JS SDK, customers call
setConversationCustomAttributes and setLabel on chatwoot:ready — before
any conversation exists. These API calls silently fail because the
backend endpoints require an existing conversation. When the visitor
sends their first message, the conversation is created without those
attributes/labels, so the message_created webhook payload is missing the
expected metadata.
This change queues SDK-set conversation custom attributes and labels in
the widget store when no conversation exists yet, and includes them in
the API request when the first message (or attachment) creates the
conversation. The backend now permits and applies these params during
conversation creation — before the message is saved and webhooks fire.
### How to test
1. Configure a web widget without a pre-chat form.
2. Open the widget on a test page and run the following in the browser
console after chatwoot:ready:
`window.$chatwoot.setConversationCustomAttributes({ plan: 'enterprise'
});`
`window.$chatwoot.setLabel('vip');` // must be a label that exists in
the account
3. Send the first message from the widget.
4. Verify in the Chatwoot dashboard that the conversation has plan:
enterprise in custom attributes and the vip label applied.
5. Set up a webhook subscriber for `message_created` confirm the first
payload includes the conversation metadata.
6. Verify that calling `setConversationCustomAttributes` / `setLabel` on
an existing conversation still works as before (direct API path, no
regression).
7. Verify the pre-chat form flow still works as expected.
Attachment webhook event payloads (`message_created`) were missing the
file extension and content type. The `extension` column existed but was
never populated, and `content_type` was not included in the payload at
all.
## What changed
- Added `before_save :set_extension` callback to extract file extension
from the filename when saving an attachment.
- Added `content_type` (from ActiveStorage) to the `file_metadata` used
in `push_event_data`.
### Before
```json
{
"extension": null,
"data_url": "...",
"file_size": 11960
}
```
### After
```json
{
"extension": "pdf",
"content_type": "application/pdf",
"data_url": "...",
"file_size": 11960
}
```
## How to reproduce
1. Send a message with a file attachment (e.g., PDF) via any channel
2. Inspect the `message_created` webhook payload
3. Observe `extension` is `null` and `content_type` is missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Pull Request Template
## Description
Adds custom tool support to v1
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
<img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x"
src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47"
/>
<img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x"
src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea"
/>
<img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38
32@2x"
src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
When a user signs up with an email they don't own and sets a password,
that password remains valid even after the real owner later signs in via
OAuth. This means the original registrant — who never proved ownership
of the email — retains working credentials on the account. This change
closes that gap by rotating the password to a random value whenever an
unconfirmed user completes an OAuth sign-in.
The check (`oauth_user_needs_password_reset?`) is evaluated before
`skip_confirmation!` runs, since confirmation would flip `confirmed_at`
and mask the condition. If the user was unconfirmed, the stored password
is replaced with a secure random string that satisfies the password
policy. This applies to both the web and mobile OAuth callback paths, as
well as the sign-up path where the password is rotated before the reset
token is generated.
Users who lose access to password-based login as a side effect can
recover through the standard "Forgot password" flow at any time. Since
they've already proven email ownership via OAuth, this is a low-friction
recovery path
* fix(whatsapp): resolve phone_number conflict when converting inbox between providers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move transfer_identifier_to inside transaction for atomic consolidation
* fix: reassign message sender before destroying merged contact
* fix: resolve identifier conflicts account-wide in adopt and consolidate paths
* fix: scope sender reassignment to moved conversations only
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Images and videos sent from Chatwoot to LINE inboxes fail to display on
the LINE mobile app — users see expired markers, broken thumbnails, or
missing images. This happens because LINE mobile lazy-loads images
rather than downloading them immediately, and the ActiveStorage signed
URLs expire after 5 minutes.
Closes
https://linear.app/chatwoot/issue/CW-6696/line-messaging-with-image-or-video-may-not-show-when-client-inactive
## How to reproduce
1. Create a LINE inbox and start a chat from the LINE mobile app
2. Close the LINE mobile app
3. Send an image from Chatwoot to that chat
4. Wait 7-8 minutes (past the 5-minute URL expiration)
5. Open the LINE mobile app — the image is broken/expired
## What changed
- **`originalContentUrl`**: switched from `download_url` (signed, 5-min
expiry) to `file_url` (permanent redirect-based URL)
- **`previewImageUrl`**: switched to `thumb_url` (250px resized
thumbnail meeting LINE's 1MB/240x240 recommendation), with fallback to
`file_url` for non-image attachments like video
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Linear Ticket
https://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanentlyhttps://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanently#comment-14e0f9ff
## Description
Browser push notifications fail with Socket::ResolutionError when the
push subscription endpoint's domain can't be resolved via DNS (e.g.,
defunct push service, transient DNS failure). This error wasn't handled
in handle_browser_push_error, so it fell through to the catch-all else
branch and got reported to Sentry on every notification attempt — 1,637
times in the last 7 days.
The dead subscription was never cleaned up or the error suppressed, so
every subsequent notification for the affected user triggered the same
Sentry alert.
Added Socket::ResolutionError to the existing transient network error
handler alongside Errno::ECONNRESET, Net::OpenTimeout, and
Net::ReadTimeout. The error is logged but not reported to Sentry, and
the subscription is kept intact in case it's a temporary DNS blip.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Verified that Socket::ResolutionError is a subclass of StandardError
and matches the when clause
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
# Pull Request Template
## Description
This PR fixes the white background bleed visible in the widget, widget
article viewer and help center when dark mode is active.
**What was happening**
While scrolling, the `<body>` element retained a white background in
dark mode. This occurred because dark mode classes were only applied to
inner container elements, not the root.
**What changed**
* **Widget:** Updated the `useDarkMode` composable to sync the `dark`
class to `<html>` using `watchEffect`, allowing `<body>` to inherit dark
theme variables. Also added background styles to `html`, `body`, and
`#app` in `woot.scss`.
* **Help center portal:** Moved `bg-white dark:bg-slate-900` from
`<main>` to `<body>` in the portal layout so the entire page background
responds correctly to dark mode, including within the widget iframe.
* **ArticleViewer:** Replaced hardcoded `bg-white` with `bg-n-solid-1`
to ensure better theming.
Fixes
https://linear.app/chatwoot/issue/CW-6704/widget-body-colour-not-implemented
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screencasts
### Before
**Widget**
https://github.com/user-attachments/assets/e0224ad1-81a6-440a-a824-e115fb806728
**Help center**
https://github.com/user-attachments/assets/40a8ded5-5360-474d-9ec5-fd23e037c845
### After
**Widget**
https://github.com/user-attachments/assets/dd37cc68-99fc-4d60-b2ae-cf41f9d4d38c
**Help center**
https://github.com/user-attachments/assets/bc998c4e-ef77-46fa-ac7f-4ea16d912ce3
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template
## Description
This PR fixes
1. Messages being trimmed to the default 1024 limit in `trimContent`
method, instead of channel-specific limits for drafts and AI tasks.
2. Telegram messages are allowed up to 10,000 characters in config, but
the API supports only 4096, causing failures for oversized messages.
Fixes
https://linear.app/chatwoot/issue/CW-6694/captain-ai-rewrite-tasks-truncate-draft-to-1024-chars-trimcontenthttps://github.com/chatwoot/chatwoot/issues/13919
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Loom video
**Before**
https://www.loom.com/share/00e9d6b4d19247febf35dffa99da3805
**After**
https://www.loom.com/share/c4900e9effc345c79bcd8a5aa1ee277b
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Adds `WebsiteBrandingService` (OSS) with an Enterprise override using
Firecrawl v2 to extract branding and business data from a URL for
onboarding auto-fill.
OSS version uses HTTParty + Nokogiri to extract:
- Business name (og:site_name or title)
- Language (html lang)
- Favicon
- Social links from `<a>` tags
Enterprise version makes a single Firecrawl call to fetch:
- Structured JSON (name, language, industry via LLM)
- Branding (favicon, primary color)
- Page links
Falls back to OSS if Firecrawl is unavailable or fails.
Social handles (WhatsApp, Facebook, Instagram, Telegram, TikTok, LINE)
are parsed deterministically via a shared `SocialLinkParser`.
> We use links for socials, since the LLM extraction was unreliable,
mostly returned empty, and hallucinated in some rare scenarios
## How to test
```ruby
# OSS (no Firecrawl key needed)
WebsiteBrandingService.new('chatwoot.com').perform
# Enterprise (requires CAPTAIN_FIRECRAWL_API_KEY)
WebsiteBrandingService.new('notion.so').perform
WebsiteBrandingService.new('postman.com').perform
```
Verify the returned hash includes business_name, language,
industry_category, social_handles, and branding with
favicon/primary_color.
<img width="908" height="393" alt="image"
src="https://github.com/user-attachments/assets/e3696887-d366-485a-89a0-8e1a9698a788"
/>
This change blocks Help Center access for default/Hacker-plan accounts
and closes the downgrade gap that could leave `help_center` enabled
after a subscription falls back to the default cloud plan.
Fixes: none
Closes: none
## Why
Default-plan accounts should not be able to access the Help Center, but
the downgrade fallback path only reset the plan name and did not
reconcile premium feature flags. That meant some accounts could keep
`help_center` enabled even after landing back on the Hacker/default
plan.
## What this change does
- blocks Help Center portal and article access for default/Hacker-plan
accounts
- reconciles premium feature flags when a subscription falls back to the
default cloud plan, so `help_center` is disabled immediately instead of
waiting for a later webhook
- preserves existing account `custom_attributes` during Stripe customer
recreation instead of overwriting them
- adds Enterprise coverage for the default-plan access checks on hosted
and custom-domain Help Center routes
- fixes the public access check to use the resolved portal object so
blocked requests return the intended response instead of raising an
error
## Validation
1. Create or use an account on the default/Hacker cloud plan with an
active portal.
2. Visit the portal home page and a published article on both the
Chatwoot-hosted URL and a configured custom domain.
3. Confirm the Help Center is blocked for that account.
4. Downgrade a paid account back to the default/Hacker plan through the
Stripe webhook flow.
5. Confirm `help_center` is disabled right after the downgrade fallback
is processed and the account can no longer access the Help Center.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
The scheduled-messages refactor (9a05ff524) split setActiveChat into
two branches but dropped the .then() callback that emits
SCROLL_TO_MESSAGE for normal conversation loads (no messageId).
Without this event scrollToBottom() never fires after messages load,
leaving the scroll at the top and triggering infinite history fetch.
Self-hosted installations were incorrectly hitting the daily email rate
limit of 100, seeded from `installation_config`. Since self-hosted users
control their own infrastructure, email rate limiting should only apply
to Chatwoot Cloud.
Closes#13913
After a successful WhatsApp OAuth reauthorization, the health check runs
immediately and finds the phone number in a pending provisioning state
(`platform_type: NOT_APPLICABLE`). This incorrectly triggers
`prompt_reauthorization!`, re-setting the Redis disconnect flag and
sending a disconnect email — even though the reauth just succeeded.
The fix skips the health check during reauthorization flows. It still
runs for new channel creation.
Closes https://github.com/chatwoot/chatwoot/pull/12556
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How to reproduce
1. Have a WhatsApp channel with a phone number in pending provisioning
state (display name not yet approved by Meta)
2. Complete the OAuth reauthorization flow
3. Observe that the user receives a "success" response but immediately
gets a disconnect email
## What changed
- `Whatsapp::EmbeddedSignupService#perform` now skips
`check_channel_health_and_prompt_reauth` when `inbox_id` is present
(reauthorization flow)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
When syncing contacts to LeadSquared, the `Lead.CreateOrUpdate` API
defaults to searching by email. If a contact has no email (or a
different email) but a phone number matching an existing lead, the API
fails with `MXDuplicateEntryException` instead of finding and updating
the existing lead. This accounted for ~69% of all LeadSquared
integration errors, and cascaded into "Lead not found" failures when
posting transcript and conversation activities (~14% of errors).
## What changed
- `LeadClient#create_or_update_lead` now catches
`MXDuplicateEntryException` and retries the request once with
`SearchBy=Phone` appended to the body, telling the API to match on phone
number instead
- Once the retry succeeds, the returned lead ID is stored on the contact
(existing behavior), so all future events use the direct `update_lead`
path and never hit the duplicate error again
## How to reproduce
1. Create a lead in LeadSquared with phone number `+91-75076767676` and
email `a@example.com`
2. In Chatwoot, create a contact with the same phone number but a
different email (or no email)
3. Trigger a contact sync (via conversation creation or contact update)
4. Before fix: `MXDuplicateEntryException` error in logs, contact fails
to sync
5. After fix: retry with `SearchBy=Phone` finds and updates the existing
lead, stores the lead ID on the contact
# Pull Request Template
## Description
This PR updates the priority icons with a new set and makes them
consistent across the app.
## How Has This Been Tested?
**Screenshots**
<img width="420" height="550" alt="image"
src="https://github.com/user-attachments/assets/cb392934-6c4d-46b4-9fde-244461da62ef"
/>
<img width="358" height="340" alt="image"
src="https://github.com/user-attachments/assets/cb18df47-9a17-42f8-9367-e8b7c4e3958d"
/>
<img width="344" height="468" alt="image"
src="https://github.com/user-attachments/assets/9de92374-e732-48eb-a8a9-85c5b5100931"
/>
<img width="445" height="548" alt="image"
src="https://github.com/user-attachments/assets/ecc4ce51-165c-4593-a9a2-e70b08a29006"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
This pull request fixes the model annotation tooling due to previous
incomplete migration from `annotate` to `annotaterb` gem (#12600). It
also improves the handling of serialized values in the
`InstallationConfig` model by ensuring a default value is set,
simplifying the code, and removing a workaround for YAML
deserialization.
**Annotation tooling updates:**
* Added `.annotaterb.yml` to configure the `annotate_rb` gem with
project-specific options, centralizing annotation settings.
* Replaced the custom `auto_annotate_models.rake` task with the standard
rake task from `annotate_rb`, and added `lib/tasks/annotate_rb.rake` to
load annotation tasks in development environments.
[[1]](diffhunk://#diff-9450d2359e45f1db407b3871dde787a25d60bb721aed179a65ffd2692e95fb4bL1-L61)
[[2]](diffhunk://#diff-578cdfc7ad56637e42472ea891ea286dff8803d9a1750afdbfeafec164d9b8b2R1-R8)
**Model serialization improvements:**
* Updated the `InstallationConfig` model to set a default value for the
`serialized_value` attribute, ensuring it always has a hash with
indifferent access and removing the need for a deserialization
workaround in the `value` method.
[[1]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L22-R22)
[[2]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L36-L39)
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Adds a Platform API endpoint that allows migrating existing Google and
Microsoft email channels (with OAuth credentials) into Chatwoot without
requiring end-users to re-authenticate. This enables customers who lack
Rails console access to programmatically migrate email channels from
legacy systems.
### How to test
1. Create a Platform App and grant it permissible access to a target
account
2. `POST /platform/api/v1/accounts/:account_id/email_channel_migrations`
with a payload like:
```json
{
"migrations": [
{
"email": "support@example.com",
"provider": "google",
"provider_config": {
"access_token": "...",
"refresh_token": "...",
"expires_on": "..."
},
"inbox_name": "Migrated Support"
}
]
}
```
3. Verify channels are created with correct provider, provider_config, and IMAP defaults
4. Verify partial failures (e.g. duplicate email) don't roll back other migrations in the batch
5. Verify unauthenticated and non-permissible requests return 401
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Webhook payloads (`message_created`, `message_updated`) started sending
channel-rendered HTML in the `content` field instead of the original raw
message content after PR #12878. This broke downstream agent bots and
integrations that expected plain text or markdown.
Closes https://linear.app/chatwoot/issue/PLA-109/webhook-payloads-send-channel-rendered-html-instead-of-raw-content
## How to reproduce
1. Connect an agent bot to a WebWidget inbox
2. Send a message with markdown formatting (e.g. `**bold**`) from the
widget
3. Observe the agent bot webhook payload — `content` field contains
`<p><strong>bold</strong></p>` instead of `**bold**`
## What changed
Split `MessageContentPresenter` into two public methods:
- `outgoing_content` — renders markdown for the target channel (used by
channel delivery services)
- `webhook_content` — returns raw content with CSAT survey URL when
applicable, no markdown rendering (used by `webhook_data`)
Updated `Message#webhook_data` to use `webhook_content` instead of
`outgoing_content`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
# Pull Request Template
## Description
Relocate controller from enterprise/ to app/ and add
Api::V1::Accounts::Captain::TasksController.prepend_mod_with for EE
overrides.
Fixes: Ai assist giving 404 on CE
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
before:
<img width="482" height="130" alt="image"
src="https://github.com/user-attachments/assets/f51dc28a-ac54-45c4-9015-6f956fdf5057"
/>
after:
<img width="458" height="182" alt="image"
src="https://github.com/user-attachments/assets/eb86a679-5482-4157-9f4e-f3e9953d8649"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Webhook retry failure logs for agent-bot now include the event payload,
making it easier to identify which event failed when debugging transient
upstream errors (429/500).
Previously the log only showed:
`[AgentBots::WebhookJob] attempt 1 failed
RestClient::InternalServerError`
Now it includes the payload:
`[AgentBots::WebhookJob] attempt 1 failed
RestClient::InternalServerError payload={"event":"message_created",...}`
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-superadmin users now see a "contact your administrator" message
instead of the "how to enable" action button on group disabled banners.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Baileys `/on-whatsapp` endpoint can return a bare JSON array instead
of a hash with a `data` key. Calling `Array#dig` with a string key
raises `TypeError (no implicit conversion of String into Integer)`,
causing a 500 error. Handle both response shapes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Pull Request Template
## Description
This PR adds support to auto-focus the editor when clicking reply to
this message, the editor now automatically receives focus so users can
start typing immediately.
Fixes
https://linear.app/chatwoot/issue/CW-6661/typing-box-not-focused-after-clicking-reply-to-message
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screencast
https://github.com/user-attachments/assets/c5e77055-3f68-4ad8-934e-cfc465166e8a
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description
The RemoveOrphanConversationsService filters orphan conversations by a
time window before deleting them. Previously it used created_at, which
could miss old conversations that still had recent activity.
Switching to last_activity_at ensures the cleanup window reflects actual
conversation activity rather than creation time.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- By running Rake task
- Run the job from console
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template
## Description
Captain v1 does not have access to contact attributes. Added a toggle to
let user choose if they want contact information available to Captain.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Specs and locally
<img width="1924" height="740" alt="CleanShot 2026-03-19 at 18 48 19@2x"
src="https://github.com/user-attachments/assets/353cfeaa-cd58-40eb-89e7-d660a1dc1185"
/>
![Uploading CleanShot 2026-03-19 at 18.53.26@2x.png…]()
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
GROUPS_ENABLED was a Ruby constant evaluated once at class load time,
so changes to the env var were not picked up on boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(scheduled-messages): add recurring scheduled messages
Implements the recurring scheduled messages feature allowing agents to
configure recurrence rules when scheduling messages, with automatic
creation of subsequent scheduled messages after each send.
Backend:
- RecurringScheduledMessage model with JSONB recurrence_rule validation
- RecurrenceCalculatorService for next occurrence date calculation
- RecurrenceDescriptionService for human-readable rule descriptions
- CreateNextOccurrenceService for auto-creating child ScheduledMessages
- RecurringScheduledMessagesController with CRUD operations
- RecurringScheduledMessagePolicy for authorization
- Modified SendScheduledMessageJob to handle recurrence after send
- Updated due_for_sending scope to exclude resolved conversations
- ActionCable events for real-time updates
- Activity message i18n (en + pt-BR)
Frontend:
- RecurrenceDropdown.vue with contextual shortcut options
- RecurrenceCustomModal.vue for custom recurrence configuration
- RecurringScheduledMessageItem.vue for sidebar display
- Integration into ScheduledMessageModal.vue
- Updated ScheduledMessages.vue with recurrence section and filtering
- Vuex store module for recurring scheduled messages
- API client for CRUD operations
- WebSocket handlers in actionCable.js
- recurrenceHelpers.js utility functions
- i18n keys for en and pt-BR
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): fix creation and edit visibility
- Fix API payload key mismatch (snake_case vs camelCase) in modal submit
- Add status: 'active' to recurring creation payload
- Fix strong params to permit recurrence_rule array fields (week_days)
- Cast string values from strong params to integers for JSONB validation
- Show RecurrenceDropdown when editing (remove isEditing gate)
- Populate recurrenceRule from scheduled message's recurring parent
- Include recurring_scheduled_message_id and recurrence_rule in
scheduled message jbuilder response
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): fix dropdown toggle and locale tag errors
- Use DropdownItem :click prop instead of @click to use the injected
closeMenu from DropdownContainer context (default slot doesn't
expose toggle)
- Normalize locale from pt_BR to pt-BR for Intl.DateTimeFormat
compatibility in RecurringScheduledMessageItem
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(recurring-messages): add separator line and expandable history
- Add border separator between recurring messages section and
pending/draft messages, matching the history section separator
- Replace static 'N enviadas' counter with clickable toggle that
expands to show individual sent/failed child messages with
status badges and formatted timestamps
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(recurring-messages): add click-to-navigate on sent children
Make sent child messages in recurring message history clickable.
Clicking navigates to the actual message in the conversation using
the messageId query param, same pattern as ScheduledMessageItem.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(recurring-messages): allow editing recurring messages
- Add edit button to RecurringScheduledMessageItem (active only)
- Transform recurring message into scheduledMessage-compatible shape
with recurring_scheduled_message_id set, so the modal reuses
the existing update path
- Handle edge case of removing recurrence from a recurring message
(cancels series without trying to update a non-existent standalone)
- Sent history is preserved by the backend update action
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): show recurrence field without date selection
Remove v-if="scheduledDate" gate so the recurrence dropdown is
always visible in the modal. Falls back to today's date for
contextual shortcut labels when no date is selected yet.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): ensure recurrence visible on edit and add pending_scheduled_message to API
- Add pending_scheduled_message to recurring_scheduled_message jbuilder
so REST API data matches WebSocket push_event_data
- Add fallback in openEditRecurringModal to find pending child from
scheduled_messages array when pending_scheduled_message is absent
- Add same fallback in nextSendLabel computed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): add sections for Drafts and Pending messages in the UI
* refactor(scheduled-messages): merge RecurringScheduledMessageItem into ScheduledMessageItem
Consolidate the recurring message card into the existing
ScheduledMessageItem component instead of maintaining a separate
component. The unified component detects recurring messages via
recurrence_rule and conditionally shows:
- Recurrence description header with repeat icon
- Next send time label
- Expandable sent/failed children history with click-to-navigate
- Stop button (replaces delete) with confirmation modal
- Active/completed/cancelled status badges
Delete the now-unused RecurringScheduledMessageItem.vue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): use blue badge for active, keep green for sent
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): remove draft and pending sections from UI and update recurrence title
* fix(recurring-messages): use Teleport for recurrence dropdown
Replace DropdownContainer with Teleport-based floating dropdown so
options render outside the modal. Fixes:
- Dropdown no longer enlarges the modal or causes scrolling
- Dropdown closes before Custom modal opens (no overlap)
- Auto-detects available space and opens above/below trigger
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): recalculate next date when recurrence rule changes
When editing a recurring message and changing the recurrence rule,
the pending occurrence date is now validated against the new rule.
If the user-provided date doesn't match (e.g. Thursday removed from
weekly days), the system computes the next valid date using
RecurrenceCalculatorService instead of blindly using the old date.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): resolve deprecated onClose and disabled type warnings
- Replace :on-close prop with @close event on woot-modal components
- Cast hasTemplate computed to boolean to fix disabled prop type check
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): replace remaining deprecated on-close props
- ScheduledMessages.vue delete confirm modal
- RecurrenceCustomModal.vue modal
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): address review feedback on PR #240
- Fix v-if/v-else bug hiding message list behind resolved warning
- Fix occurrences_sent incrementing on failed sends (skip_increment flag)
- Fix compute_next_valid_date using .min instead of .max
- Fix Vuex delete action to update state on cancel (not remove)
- Use atomic update_counters for occurrences_sent increment
- Add safe Date.iso8601 parsing with rescue in should_complete?
- Add null: false to occurrences_sent migration column
- Fix pt-BR accent: Recorrencia → Recorrências
- Use I18n.with_locale(account.locale) for all activity messages
- Fix N+1 in jbuilder partials (Ruby filtering + eager loading)
- Add interval >= 1 validation to RecurrenceCustomModal isValid
- Validate recurrence_rule presence when status is active
- Add ISO8601 date format validation for end_date
- Add unknown_agent i18n key for fallback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): wrap create/update in transactions, clean pending on deactivation
- Wrap create and update flows in ActiveRecord transactions
- Move attachment purge after save! to prevent data loss on validation failure
- Destroy pending children when status transitions to non-active
- Fixes critical bug where stopping recurrence could leave armed pending messages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): prevent monthly/yearly day-of-month drift
Store the original intended day in recurrence_rule JSONB as month_day
(for monthly) and year_day/year_month (for yearly). The calculator
now uses these stored values instead of @last_date.day, preventing
drift after short months cap the day (e.g., Jan 31 → Feb 28 → all
subsequent months stuck on 28).
Backend:
- RecurrenceCalculatorService: use rule[:month_day] for monthly and
rule[:year_day]/rule[:year_month] for yearly calculations
- Controller: permit and cast the new integer keys
Frontend:
- recurrenceHelpers: yearly shortcuts include year_day/year_month
- RecurrenceCustomModal: emit month_day for monthly day_of_month
rules and year_day/year_month for yearly rules
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): i18n description services, align due_for_sending scope
Backend:
- RecurrenceDescriptionService: replace hardcoded English with I18n.t()
calls; accept locale parameter and use I18n.with_locale
- RecurringScheduledMessage model: pass account locale to description service
- ScheduledMessage: align due_for_sending? instance method with scope by
checking conversation status (open/pending)
Frontend:
- buildRecurrenceDescription: use t() i18n function instead of manual
isPt locale branching
- Add DESCRIPTION i18n keys to en/conversation.json and pt_BR/conversation.json
- Update RecurrenceDropdown and ScheduledMessageItem callers to pass t
i18n:
- Add recurring_scheduled_messages.description.* keys to en.yml and pt_BR.yml
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): keep pending child when deactivating recurrence
When editing a recurring message to disable recurrence while setting a
future send date, the pending child message is now preserved instead of
being destroyed. This allows a 'final send' without creating new
recurrences (the send job already guards with recurring&.active?).
Backend:
- Add update_pending_on_deactivation: updates pending child's
scheduled_at or creates a final pending occurrence
- Replace destroy_all in update's non-active branch
Frontend:
- activeRecurringMessages now includes non-active recurring messages
that still have a pending child (pending_scheduled_message)
- Stop button hidden for already-cancelled recurring messages
- inactiveRecurringMessages excludes messages with pending children
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): prevent removing recurrence from existing recurring message
Once a scheduled message has recurrence, the 'Does not repeat' option is
hidden from the RecurrenceDropdown when editing. This avoids edge cases
where deactivating recurrence leaves the message in an ambiguous display
state.
- RecurrenceDropdown: add hideNoRepeat prop, filter NO_REPEAT from shortcuts
- ScheduledMessageModal: pass hideNoRepeat when isEditingRecurring
- Revert update_pending_on_deactivation (no longer reachable from UI)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): address CodeRabbit review feedback
- ScheduledMessage#push_event_data: expose recurring_scheduled_message_id
in ActionCable payloads so frontend correctly classifies children
- RecurringScheduledMessagePolicy: add agent_bot? check for parity with
ScheduledMessagePolicy
- RecurrenceCalculatorService: guard against nil/empty week_days
- Factory: bind inbox and account to conversation to prevent cross-account
flakiness in specs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(recurring-messages): update schema to enforce non-null constraint on occurrences_sent
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
* feat(scheduled-messages): add predefined time shortcuts for scheduling
Replace the manual date/time picker with predefined day and time period
chip selectors for faster message scheduling.
Day shortcuts: Today, Tomorrow, This weekend (Sat), Next week (Mon),
Next weekend (next Sat), Next month (1st), Custom (date picker fallback).
Time period shortcuts: Morning (8:00), Afternoon (13:00), Evening (18:00).
Each day chip shows the corresponding date (dd/MM) in secondary color.
Past time periods for 'Today' are automatically disabled.
The existing date picker is preserved as the 'Custom' option.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scheduled-messages): use dropdown selectors and remove header
Replace chip/button selectors with native <select> dropdowns for day
and time period selection. Remove the 'Date and time to send' header
from the modal since the dropdown labels serve as placeholders.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): close datepicker on confirm and click-outside
Removed manual open/close state management and @click.stop that was
blocking click-outside detection. Added confirm prop so the picker
has an explicit OK button and auto-closes properly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): initialize custom mode when editing existing schedule
When editing a scheduled message, the ScheduleDateShortcuts component
now detects the pre-existing datetime and opens in Custom mode with
the datepicker pre-filled, preserving the original date and time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): use locale-aware date format in shortcuts
Replace hardcoded dd/MM format with Intl.DateTimeFormat using
navigator.language, matching the existing locale-aware pattern
in DatePickerHelper.js. Removes unused date-fns format import.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): use app locale for date formatting
Replace navigator.language with the i18n app locale for shortcut date
labels and datepicker calendar. Add getDatePickerLang helper that
generates locale-aware day/month names via Intl.DateTimeFormat.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): normalize locale tag to BCP 47 format
Chatwoot uses underscore locale tags (pt_BR) but Intl.DateTimeFormat
requires BCP 47 hyphens (pt-BR). Add toBcp47 normalizer to prevent
RangeError: invalid language tag.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): locale-aware time period display
Add formatHour helper using Intl.DateTimeFormat so time periods
show 8:00/13:00/18:00 in pt-BR and 8:00 AM/1:00 PM/6:00 PM in en.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(scheduled-messages): add unit tests for scheduleDateShortcutHelpers
Cover getShortcutDate (weekday/Saturday/Sunday edge cases),
applyTimePeriod, isTimePeriodPast, formatShortDate, formatHour,
getDatePickerLang, and getDayShortcutOptions including locale
normalization for underscore tags like pt_BR.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): sync customDateTime when modelValue changes in custom mode
Keep DatePicker in sync when the parent changes modelValue while
already in Custom mode (e.g. switching between scheduled messages).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): add aria-labels to schedule dropdown selects
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): use empty string instead of null for dateTimeError
Avoids Vue prop validation warning since ScheduleDateShortcuts
declares dateTimeError as a String prop.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scheduled-messages): simplify to 3 fixed schedule shortcuts
Replace two dropdown selectors (6 day options × 3 time periods) with
3 pre-computed clickable shortcut buttons:
- Tomorrow morning (08:00)
- Tomorrow afternoon (13:00)
- Monday morning (08:00)
Each shortcut shows the exact calendar date and time for clarity.
Special Sunday rule: 'Monday' points to next week's Monday since
'Tomorrow' already covers the immediate Monday.
The 'Custom' option with full DatePicker is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scheduled-messages): Gmail-style list design for schedule shortcuts
Replace chip buttons with full-width clickable rows in a bordered
container, matching Gmail's 'Schedule send' dialog pattern:
- Label on left, formatted date/time on right in gray
- Calendar icon for the custom date/time option
- Subtle border separators between rows
- Selected state with blue highlight
Also improves date formatting from '15/03' to '15 de mar.' using
month short names for better readability.
Updates i18n:
- PT_BR: 'Amanhã à tarde', 'Escolher data e hora'
- EN: 'Choose date and time'
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scheduled-messages): improve modal layout and datepicker positioning
- Add 'Schedule send' / 'Programar envio' section header above the
schedule shortcuts for clear visual identification
- Move attachment/template controls next to the message editor so
content-related actions stay grouped together
- Move datepicker outside the bordered shortcut container to avoid
cramped positioning; add rounded-xl and proper text sizing
- Add i18n key SCHEDULE_LABEL (en: 'Schedule send', pt_BR: 'Programar envio')
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(schedule): replace popup datepicker with inline calendar
Replace the popup-style vue-datepicker-next with an inline calendar
(same approach used by the snooze feature), rendering the calendar
directly within the modal for a more intuitive UX.
- Switch DatePicker to inline mode (no popup/z-index issues)
- Add disablePastTimes validation (prevents selecting past times)
- Full-width responsive calendar with scoped deep styles
- Remove unused DATETIME_PLACEHOLDER and DATETIME_FORMAT i18n keys
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(schedule): replace datepicker with natural language input
Replace the broken inline datepicker with a natural language text input
powered by chrono-node, similar to Chatwoot's upcoming snooze UX.
Users can now type dates naturally:
- EN: 'tomorrow at 2pm', 'next friday morning', 'in 3 hours'
- PT: 'amanhã às 14h', 'próxima sexta de manhã', '20 de março às 10h'
Changes:
- Add chrono-node dependency for natural language date parsing
- Add preProcessDateInput() to normalize PT time expressions (8h→8:00,
de manhã→8:00, à tarde→13:00, de noite→18:00)
- Add parseNaturalDate() with locale-aware parsing (PT/EN)
- Add formatFullDateTime() for parsed date preview display
- Replace DatePicker with text input + real-time parsed date preview
- Show green checkmark when date is valid, amber warning if in the past,
hint text if input is unrecognizable
- Remove unused getDatePickerLang() and vue-datepicker-next import
- Remove unused DATETIME_PLACEHOLDER/DATETIME_FORMAT i18n keys
- Update i18n: CUSTOM label, placeholder, hint, and past-date warning
- Update tests: 40 tests covering new functions (was 29)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): improve PT natural language preprocessing
Normalize accent-less input common in casual Brazilian Portuguese:
- 'amanha' → 'amanhã', 'sabado' → 'sábado', 'proxima' → 'próxima'
- 'as' → 'às' before digits or time-of-day words (e.g. 'as 19h')
- Support 'pela manhã/tarde/noite' and 'no período da manhã/tarde/noite'
Previously 'Amanhã as 19h' failed because chrono-node requires 'às'
(with accent) as a time connector. Now all common casual PT patterns
work reliably.
Tests: 50 passing (was 40)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): use forwardDate to always parse weekdays as upcoming
chrono-node defaults to the *most recent* occurrence of a weekday,
so 'sexta' on Monday returned last Friday (past). Adding
{ forwardDate: true } makes it always return the next occurrence.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(schedule): add datepicker fallback and dual-language parsing
- Add discrete calendar icon button next to text input that toggles
an inline vue-datepicker-next with date+time selection
- Try both chrono.pt and chrono (EN) parsers, pick the one that
matches more of the input text — supports mixed-language input
like 'quarta 10am' or 'friday às 14h'
- Insert 'às' connector between PT weekday names and bare numbers
so 'quarta 10' parses correctly (chrono.pt requires the connector)
- Add DATEPICKER_TOOLTIP i18n key (EN + PT_BR)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): toggle between text input and datepicker views
When calendar button is clicked, hide the natural language text input
and show the inline datepicker full-width. A small 'Type a date and
time' link below the calendar lets users switch back to text input.
Calendar button is vertically centered with the input field (size-[34px]
matches input height).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): align calendar button with text input height
Use self-stretch instead of fixed size so the button stretches to
match the input height in the flex row, eliminating misalignment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): adjust margin for custom text input in date shortcuts
* fix(schedule): use popup datepicker with datetime and confirm
Replace the inline datepicker toggle with a popup DatePicker that
opens directly from the calendar button. Uses type='datetime' with
confirm mode so users can pick both date and time. On confirm, the
selected datetime populates the natural language text field. The text
input and calendar button are always visible side by side.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): hide seconds column in datepicker
Add :show-second='false' to only show hour and minute selection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(schedule): adjust DatePicker width for better responsiveness
* refactor(schedule): use locale for parser priority and add aria-label
- parseNaturalDate now uses locale to call the matching parser first
(chrono.pt for PT, chrono for EN) before falling back to the other,
removing the eslint-disable comment for unused locale param
- Add aria-label to the natural language date input for screen readers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove leftover planning files
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
* feat(scheduled-messages): scroll to sent message from sidebar
- Expose message_id in JBuilder serialization and push_event_data
- Add HIGHLIGHT_MESSAGE bus event for in-page message highlighting
- Add 'Go to message' button on sent scheduled messages in sidebar
- Enhance onScrollToMessage to fetch messages around target when not in DOM
- Extend Message.vue highlight to work with bus events (not just route query)
- Add i18n keys for EN and pt-BR
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scheduled-messages): make sent card clickable instead of button
Replace the 'Go to message' button with a clickable card. The entire
sent scheduled message card now has cursor-pointer, hover highlight,
and a tooltip — clicking anywhere on it scrolls to the message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): address PR review feedback
- Use camelCase value for HIGHLIGHT_MESSAGE bus event ('highlightMessage')
- Show toast alert when message not found after fetch or on fetch error
- Use the MESSAGE_NOT_FOUND i18n key that was previously unused
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): use messageId query param for find message
Replace direct bus event emission with route navigation using
?messageId= query param, reusing the same proven mechanism used by
search results and copy-message-link.
Changes:
- ScheduledMessageItem: router.replace with ?messageId= instead of
emitting SCROLL_TO_MESSAGE directly
- ConversationView: handle ?messageId= on same-conversation (was
previously skipped), fetch messages around target and scroll
- MessagesView: clean up ?messageId= from URL after scroll/error
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scheduled-messages): add toast feedback for find message
Show a persistent "Searching for message..." toast while fetching,
auto-dismissed on success. Show "Message not found" error toast if
the message cannot be located.
Uses usePendingAlert for the loading state in both ConversationView
(initial fetch) and MessagesView (fallback fetch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: prevent scroll overshoot when navigating to message
Remove the immediate fetchPreviousMessages() call after
scrollIntoView({ behavior: smooth }). The fetch was prepending
messages above the target while the smooth scroll animation was
still running, shifting the DOM and causing the scroll to stop
short of the target message. The scroll event handler will
naturally trigger message loading when the user scrolls up later.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(scheduled-messages): remove redundant clearMessageIdFromRoute calls
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: add group and conversation types to contacts and conversations, and implement conversation group membership model
* chore: add factory and specs for conversation group member model
* chore: add group type checks and associations for contacts and conversations
* refactor: remove scopes from ConversationGroupMember model
* refactor: remove scopes from ConversationGroupMember model specs
* refactor: enhance conversation type migration with concurrent indexing
* feat: add is_active index and scopes to ConversationGroupMember model
* feat: implement GroupConversationHandler for managing group conversations
* feat: add group_type attribute to contact creation
* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid
* feat: handle group JID format in remote_jid method
* chore: update group contact info when finding or creating group contact
* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation
* feat: implement group message handling and metadata fetching in WhatsApp service
* chore: add spec for group type handling in contact creation for individual and group contacts
* chore: add specs for test scopes in conversation group members
* chore: update documentation for sender phone extraction in group conversation handler
* chore: move GroupConversationHandler concern to correct dir
* chore: implement specs for recipient_id handling to individual and group contacts
* chore: add group message handling specs for incoming messages
* chore: enhance group message handling to prevent race conditions
* chore: add group_metadata method to with error handling
* chore: add test for sending messages to group recipients in WhatsappBaileysService
* chore: raise error for unsuccessful response in group_metadata method
* chore: adds tests for group metadata retrieval and error handling
* chore: refactor build_sender_contact_attributes to avoid double call methods
* chore: update error handling for attachment download failure in message creation
* chore: optimize update_contact_info method to use compact hash for updates
* chore: simplify find_or_create_sender_contact method return values
* chore: rename group and individual contact message handlers
* chore: remove pointless comments from group contact message handler methods
* chore: refine sender JID extraction logic to remove unnecessary checks
* chore: remove phone number in spec for group contact attributes
* chore: implement sync_group route
* chore: implement get group_members route
* fix: sync_group participants creation handling
* chore: update contact avatar handling in group message processing
* chore: move sync_group functionality for conversation model
* feat: add sync_group action to ConversationsController and route
* fix: set contact name to phone in group message processing
* chore: refine group member retrieval logic in sync_group service and view
* feat: implement group participants update handling
* feat: implement group updates handling and localization for group activities
* chore: add handling for group membership requests and icon changes
* chore: add authorization for sync_group action in ContactsController
* chore: add sync_group endpoint specs for contact management
* chore: add authorization for sync_group action in ConversationsController
* chore: add specs for sync_group endpoint in ConversationsController
* chore: refactor index action in GroupMembersController for improved conversation filtering
* chore: add request specs for group_members endpoint in ContactsController
* chore: add specs for sync_group method in Conversation model
* chore: add specs for sync_group method in Channel::Whatsapp model
* chore: remove comments in find_or_create_group_conversation method
* chore: add specs for Contacts::SyncGroupService to validate group contact behavior
* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates
* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions
* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService
* chore: add specs for group membership request and icon change handling in MessagesUpsert
* chore: add specs for sync_group method to handle group metadata and participant updates
* chore: update sync_group method to retrieve group members and adjust JSON response
* chore: update conversation query to filter by group type in GroupMembersController
* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group
* chore: update find_or_create_group_conversation to include pending conversations
* chore: refactor sync_group method and enhance specs for group conversation handling
* feat: add GroupEventHelper module for managing group activities and contacts
* chore: refactor group contact inbox and conversation creation methods in group handlers
* chore: remove unnecessary check for blank participant contacts in sync_group_members method
* feat: implement message receipt update handling for WhatsApp integration
* chore: resolve rubocop rule for update_last_seen_at method
* chore: update swagger with endpoints for syncing group information and listing group members
* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger
* chore: include participant information in reaction and quoted message keys for send message in group conversations
* chore: enhance whatsapp_baileys_service with participant handling for message keys
* feat: add skill for writing RSpec tests in the project
* fix: update recipient_id logic to directly use contact identifier for group contacts
* chore: implement group stub message handling for membership requests and icon changes
* fix: update whatsapp inbox source_id validation regex spec
* chore: fix spec for contact syncing group
* chore: remove readTimestamp handling and related tests for message read updates in group
* Cayo oliveira/cu 86af01932/4 backend gerenciamento dos grupos (#221)
* feat: add is_active index and scopes to ConversationGroupMember model
* feat: implement GroupConversationHandler for managing group conversations
* feat: add group_type attribute to contact creation
* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid
* feat: handle group JID format in remote_jid method
* chore: update group contact info when finding or creating group contact
* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation
* feat: implement group message handling and metadata fetching in WhatsApp service
* chore: add spec for group type handling in contact creation for individual and group contacts
* chore: add specs for test scopes in conversation group members
* chore: update documentation for sender phone extraction in group conversation handler
* chore: move GroupConversationHandler concern to correct dir
* chore: implement specs for recipient_id handling to individual and group contacts
* chore: add group message handling specs for incoming messages
* chore: enhance group message handling to prevent race conditions
* chore: add group_metadata method to with error handling
* chore: add test for sending messages to group recipients in WhatsappBaileysService
* chore: raise error for unsuccessful response in group_metadata method
* chore: adds tests for group metadata retrieval and error handling
* chore: refactor build_sender_contact_attributes to avoid double call methods
* chore: update error handling for attachment download failure in message creation
* chore: optimize update_contact_info method to use compact hash for updates
* chore: simplify find_or_create_sender_contact method return values
* chore: rename group and individual contact message handlers
* chore: remove pointless comments from group contact message handler methods
* chore: refine sender JID extraction logic to remove unnecessary checks
* chore: remove phone number in spec for group contact attributes
* chore: implement sync_group route
* chore: implement get group_members route
* fix: sync_group participants creation handling
* chore: update contact avatar handling in group message processing
* chore: move sync_group functionality for conversation model
* feat: add sync_group action to ConversationsController and route
* fix: set contact name to phone in group message processing
* chore: refine group member retrieval logic in sync_group service and view
* feat: implement group participants update handling
* feat: implement group updates handling and localization for group activities
* chore: add handling for group membership requests and icon changes
* chore: add authorization for sync_group action in ContactsController
* chore: add sync_group endpoint specs for contact management
* chore: add authorization for sync_group action in ConversationsController
* chore: add specs for sync_group endpoint in ConversationsController
* chore: refactor index action in GroupMembersController for improved conversation filtering
* chore: add request specs for group_members endpoint in ContactsController
* chore: add specs for sync_group method in Conversation model
* chore: add specs for sync_group method in Channel::Whatsapp model
* chore: remove comments in find_or_create_group_conversation method
* chore: add specs for Contacts::SyncGroupService to validate group contact behavior
* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates
* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions
* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService
* chore: add specs for group membership request and icon change handling in MessagesUpsert
* chore: add specs for sync_group method to handle group metadata and participant updates
* chore: update sync_group method to retrieve group members and adjust JSON response
* chore: update conversation query to filter by group type in GroupMembersController
* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group
* chore: update find_or_create_group_conversation to include pending conversations
* chore: refactor sync_group method and enhance specs for group conversation handling
* feat: add GroupEventHelper module for managing group activities and contacts
* chore: refactor group contact inbox and conversation creation methods in group handlers
* chore: remove unnecessary check for blank participant contacts in sync_group_members method
* chore: update swagger with endpoints for syncing group information and listing group members
* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger
* fix: update recipient_id logic to directly use contact identifier for group contacts
* chore: implement group stub message handling for membership requests and icon changes
* fix: update whatsapp inbox source_id validation regex spec
* chore: fix spec for contact syncing group
* fix: optimize update_last_seen_at method to use update_columns
* feat: Implement full frontend and backend support for group conversations
- Added PRD for group conversations detailing frontend and backend requirements.
- Created new Baileys TypeScript definitions for group-related functions.
- Renamed `conversation_type` to `group_type` in the database and updated all references.
- Implemented API serialization for `group_type` in conversation and contact responses.
- Developed Vuex store module for managing group members.
- Created UI components for group management, including group creation, member management, and metadata editing.
- Integrated @mention functionality for group conversations and real-time updates via ActionCable.
* feat: [US-001] - Rename conversation_type to group_type on conversations
- Add migration to rename column and indexes
- Update Conversation model enum to group_type
- Update GroupConversationHandler concern
- Update controllers (contacts, group_members)
- Update all backend specs
* chore: mark US-001 complete, update progress log, fix rubocop annotation
* feat: [US-002] - Serialize group_type fields in API responses
* feat: [US-003] - Add group_type filter to conversations index
* feat: [US-004] - Add group_type to filter_keys.yml and FilterService
* feat: US-005 - Backend group creation endpoint
- Add POST /api/v1/accounts/:account_id/groups endpoint
- Add Groups::CreateService to orchestrate Baileys group creation
- Extend WhatsappBaileysService and BaseService with group management methods
- Add routes for group members, metadata, invite, and join requests
- Returns 403 when agent lacks inbox access, 422 when provider is unavailable
* feat: US-006 - Backend add/remove members and role management endpoints
- Add create/destroy/update actions to GroupMembersController
- Delegate group management methods from Channel::Whatsapp to provider_service
- create adds members via Baileys and creates ConversationGroupMember records
- destroy removes a member by ID and sets is_active false
- update promotes/demotes a member and updates their role
* feat: US-007 - Backend group metadata update endpoint
- Add PATCH /contacts/:id/group_metadata endpoint
- Updates group subject via Baileys and syncs contact name
- Updates group description via Baileys and syncs additional_attributes.description
- Returns 422 when provider is unavailable
* feat: US-008 - Backend invite link management endpoints
- Add GET /contacts/:id/group_invite to retrieve current invite code/url
- Add POST /contacts/:id/group_invite/revoke to revoke and get new invite code/url
- Returns 422 when provider is unavailable
* feat: US-009 - Backend join request management endpoints
- Add GET /contacts/:id/group_join_requests to list pending join requests
- Add POST /contacts/:id/group_join_requests/handle to approve/reject requests
- Uses request_action param to avoid conflict with Rails reserved params[:action]
- Returns 422 when provider is unavailable
* feat: US-010 - Extend MentionService for contact mentions
- Extract mention://contact/ID/Name URIs from message content
- Store mentioned contact IDs in message.content_attributes[mentioned_contacts]
- Existing user/team mention handling unchanged
* feat: US-011 - Frontend API clients for all group endpoints
- Add app/javascript/dashboard/api/groupMembers.js
- Exports 11 methods: getGroupMembers, syncGroup, createGroup, updateGroupMetadata,
addMembers, removeMembers, updateMemberRole, getInviteLink, revokeInviteLink,
getPendingRequests, handleJoinRequest
* feat: US-012 - Frontend Vuex store module groupMembers
- Add groupMembers store module with fetch, sync, addMembers, removeMembers, updateMemberRole actions
- Add SET_GROUP_MEMBERS and SET_GROUP_MEMBERS_UI_FLAG mutation types
- Register module in store index
* feat: US-013 - Frontend i18n keys for group features
- Add groups.json with keys for group info, filter, creation modal, metadata editing, invite link, member management, join requests, and mention dropdown
- Register groups.json in i18n locale en/index.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: US-014 - Frontend group_type filter in ConversationBasicFilter
- Add chatGroupTypeFilter state, getter, mutation, and action to conversations store
- Add getChatGroupTypeFilter getter
- Add group_type param to ConversationApi.get()
- Add Type filter section to ConversationBasicFilter with All/Individual/Group options
- Persist group_type to UI settings under conversations_filter_by.group_type
- Restore group_type from UI settings on page load
- Include groupType in conversationFilters and pass as group_type param to API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: US-013 - Frontend — i18n keys for group features (en + pt-BR)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: [US-014] - Frontend — add group_type filter to ConversationBasicFilter
All implementation was already in place from prior work:
- ConversationBasicFilter.vue has Type section with All/Individual/Group options
- ChatList.vue handles group_type in conversationFilters and restores from UI settings
- Store has setChatGroupTypeFilter action, getChatGroupTypeFilter getter
- API maps groupType → group_type query param
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-015 - Frontend — add group_type to advanced filter system
- Add GROUP_TYPE to CONVERSATION_ATTRIBUTES in filterHelper.js
- Add group_type filter definition in provider.js (components-next)
- Add group_type to legacy advancedFilterItems/index.js and filterAttributeGroups
- Add group_type to automationHelper conditionFilterMaps
- Add group_type to customViewsHelper getValuesForFilter
- Add group_type options to ChatList setParamsForEditFolderModal
- Add GROUP_TYPE i18n key in en and pt_BR advancedFilters.json
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-016 - Frontend — GroupContactInfo basic display
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-017 - Frontend — GroupContactInfo sync button
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-018 - integrate GroupContactInfo in ContactPanel
- Import GroupContactInfo component
- Conditionally render GroupContactInfo when group_type === 'group'
- Keep ContactInfo for individual conversations (no regression)
- Dynamic sidebar title: 'Group' for groups, 'Contact' for individual
- contact_notes and contact_attributes accordion sections unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-019 - Frontend — group creation UI modal
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-020 - Frontend — member management UI in GroupContactInfo
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-021 - Frontend — group metadata editing UI
Add inline editing for group name, description, and avatar in GroupContactInfo:
- Click group name to edit inline, save on Enter/blur
- Click description to edit inline with textarea, save on blur
- Click avatar to open file picker for upload via contacts/update
- Loading states on all fields during save
- Success/error alerts for all operations
- updateGroupMetadata action added to groupMembers store
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-022 - Frontend — invite link management UI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-023 - Frontend — join request management UI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-024 - Frontend — group message bubbles: sender name with color
- Add sender name display above incoming message bubbles in group conversations
- Deterministic color per sender using AVATAR_COLORS palette (name.length % 6)
- Sender name hidden for consecutive messages from the same sender
- Individual conversation bubbles unchanged
- Pass groupWithPrevious and isGroupConversation props through MessageList → Message
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-025 - Frontend — group message bubbles: sender avatar
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: Add Ralph Wiggum AI agent script for managing tool execution and progress tracking
* feat: US-026 - Frontend — @mention dropdown for group conversations
- Create TagGroupMembers.vue component for group member mention suggestions
- Modify Editor.vue: add isGroupConversation/groupContactId props, render
TagGroupMembers for group non-private context
- Modify ReplyBox.vue: compute isGroupConversation and groupContactId from
currentChat, pass to WootMessageEditor
- @ mention plugin isAllowed now triggers for group conversations
- In individual conversations or private notes, existing behavior unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-027 - Frontend — mention rendering in group message bubbles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-028 - Frontend ActionCable handler for contact.group_synced event
- Backend: Include group_members data in contact.group_synced ActionCable payload
- Frontend: Register contact.group_synced handler in ActionCableConnector
- Frontend: Add setGroupMembers action to groupMembers store for direct commits
- Tests: ActionCable handler spec + groupMembers store spec for new action
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: Update progress tracking for group conversations feature - mark tests as passing
* fix: sender click case mismatch and filter dropdown spacing
- Message.vue: use case-insensitive comparison for sender type check
(Contact.push_event_data returns 'contact' but SENDER_TYPES.CONTACT is 'Contact')
- ConversationBasicFilter.vue: replace last:mt-4 with flex-col gap-4
for consistent spacing between all three filter sections
* fix: four bugs found during manual testing review
- ContactPanel.vue: fix i18n key GROUP.INFO.SIDEBAR_TITLE → GROUP.SIDEBAR_TITLE
- groupMembers.js API: fix syncGroup HTTP method GET → POST to match backend route
- group_members_controller.rb: remove SyncGroupService from index action
- filterHelpers.js: add missing group_type case to getValueFromConversation
* docs: update progress with bug fix learnings
* chore: implement group creation functionality in UI components
* chore: add copy invite link functionality and update UI components
* feat: US-041 - Backend — ensure group_type is set on existing contacts and conversations
GroupConversationHandler#update_group_contact_info now sets group_type: :group
on contacts that are incorrectly typed as individual.
GroupConversationHandler#find_or_create_group_conversation updates existing
conversation's group_type to :group if it is currently :individual.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: mark US-041 as complete
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-029 - i18n keys for You badge and group settings (en + pt-BR)
All i18n keys already existed from prior iterations. Verified presence
and updated PRD status.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-030 - fix Baileys API route/method mismatches
Fix on_whatsapp to dig('data') before accessing first element.
Update spec stubs to match { data: [...] } response envelope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-031 - group_leave, group_setting_update, group_join_approval_mode methods
All methods, delegates, and error handling already implemented.
Verified specs pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-032 - persist group settings, invite code, and profile picture during sync
Add try_update_group_avatar to fetch and attach group profile picture
during sync_group. Update spec stubs for profile-picture-url endpoint.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-033 - GroupSettingsController with leave, update, toggle
Controller and routes already implemented. Verified rubocop passes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-034 - remove inbox_contact_id from provider_config and jbuilder
Already removed in prior iterations. Verified no references remain.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-035 - refactor TagGroupMembers to phone_number matching
Already implemented. Verified excludePhoneNumber prop and filtering.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-036 - remove InboxContact.vue and settings tab
Already removed in prior iterations. Verified no references remain.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-037 - add You badge in GroupContactInfo member list
Already implemented with isOwnMember check and blue badge styling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-038 - fix inline edit for group name and description
Already implemented with phone number normalization. Verified code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-039 - group settings section UI with toggles
Already implemented. Settings toggles, API calls, and i18n verified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-040 - leave group UI with confirmation and auto-resolve
Already implemented. Leave button, confirmation, and API call verified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-050 - Create GroupMember model and migration
New group_members table with group_contact_id, contact_id, role, is_active.
Unique index on (group_contact_id, contact_id). Associations added to Contact.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-064 - Helper method to find channel from group contact
Add Contact#group_channel to decouple channel lookup from conversations.
Update GroupMembersController and GroupSettingsController to use it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-052 - Update GroupConversationHandler to use GroupMember
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-057 - Update GroupMembersController to query GroupMember
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-058 - Update GroupSettingsController to not depend on conversations
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-060 - Update group_members jbuilder views
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-059 - Remove group_members association from Conversation model
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-051 - Remove ConversationGroupMember model and table
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: mark all stories complete, update progress
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(groups): real-time group panel, avatar refresh on icon change, editable name/description
- Add group_type to Conversations::EventDataPresenter#push_data and
Contact#push_event_data so WebSocket events carry the field, enabling
the frontend to switch to GroupContactInfo in real-time
- Update handle_icon_change_stub to call try_update_group_avatar with
force: true, purging the cached avatar and fetching the new one
- Add force parameter to try_update_group_avatar to support re-fetching
- Remove isInboxAdmin gate from name/description editing in
GroupContactInfo so any user can click to edit (server validates)
* fix(groups): rewrite SyncGroupService and simplify group metadata channel lookup
- Rewrite SyncGroupService to use contact.group_channel directly instead
of iterating conversations; find or create a conversation for sync
- Simplify GroupMetadataController to use @contact.group_channel instead
of querying conversations; remove local contact/attribute updates since
the Baileys API handles persistence via webhook events
* feat(groups): resolve conversations when inbox phone leaves or is removed
- Add resolve_conversations_if_inbox_left to GroupParticipantsUpdate
- Resolves all open/pending conversations when the inbox phone number
is removed from or leaves a group
* feat(groups): add paginated member list with infinite scroll
- Backend: add pagination to GroupMembersController (page/per_page,
default 10, ordered admins first); add meta with pagination info
to the jbuilder response
- Frontend: update groupMembers API to accept page param; add
APPEND_GROUP_MEMBERS and SET_GROUP_MEMBERS_META mutations; implement
paginated fetch with append and isFetchingMore flag in store
* feat(groups): support Ctrl+Click on group message sender to open in new tab
- navigateToGroupSender now accepts the event and checks for
Ctrl/Cmd+Click to open the sender contact in a new tab
* chore(i18n): update leave group confirmation text in en and pt_BR
* fix(groups): handle phone format differences in You badge and admin detection
- Extract phonesMatch helper that compares last 8 digits as fallback,
handling Brazilian 9th digit discrepancy (e.g. +5587988465072 vs
+558788465072)
- Apply to both isOwnMember and isInboxAdmin computed properties
* feat(groups): auto-sync members on mount, show existing members immediately
- On mount, fetch existing DB members first so they display instantly
- Then silently attempt a background sync to refresh from WhatsApp
- If sync fails (e.g. WhatsApp disconnected), existing members remain
displayed without any user-facing error
* fix(groups): pin own member on first page and return inbox phone in meta
The "You" badge was not appearing because the inbox's own member could be
missing from the first paginated page in large groups (admins sorted first).
Backend:
- Pin the inbox's own member at the top of page 1 regardless of sort order
- Return inbox_phone_number in the group members meta response
- Use last-8-digit SQL fallback for Brazilian 9th-digit phone mismatches
Frontend:
- Use meta.inbox_phone_number for the inboxPhone computed
- Fix declaration order to satisfy no-use-before-define lint rule
* fix(groups): fix member action dropdown clipped by overflow container
The promote/demote/remove dropdown menu was invisible because the member
list had `overflow-y-auto max-h-80`, clipping any absolutely-positioned
dropdown rendered inside it.
- Remove overflow container from member list; let the sidebar scroll
- Replace scroll-based infinite loading with IntersectionObserver on a
sentinel element for cleaner pagination trigger
- All member action logic (promote, demote, remove) was already wired;
the dropdown is now visible on hover
* fix(groups): keep member action dropdown visible when menu is open
The opacity-0/group-hover classes on the action menu wrapper caused the
DropdownMenu to become invisible as soon as the mouse left the row.
Now the wrapper stays fully opaque while the menu is active.
* fix(groups): move clickaway to member list wrapper to prevent instant close
v-on-clickaway was bound to every member's action div individually.
Clicking the three-dot button on one member fired closeMemberMenu from
all other members' clickaway handlers, closing the menu instantly.
Moved the directive to the single member list container instead.
* feat: add WhatsApp mention conversion (incoming + outgoing)
- New MentionConverterService for bidirectional mention handling
- Incoming: converts @phone/mentionedJid to mention://contact/ URIs
- Outgoing: extracts mention://contact/ URIs into WhatsApp mentions array
- Supports @everyone/todos group mentions
- WhatsApp renderer preserves mention display text instead of raw URI
* fix: preserve mention display text in WhatsApp renderer
mention:// URIs now render as display name text instead of the raw URL
when converting markdown to WhatsApp format
* feat: add @everyone mention option in group conversations
- Everyone item shown at top of mention dropdown
- Searchable by 'all', 'todos', 'everyone' keywords
- i18n keys added for en and pt-BR
* refactor: use Switch component for group settings toggles
- Add disabled prop to Switch component
- Replace custom toggle buttons in GroupContactInfo with Switch
- Loading spinner shown alongside toggle while toggling
* feat(whatsapp): add group sync status tracking (group_left, group_last_synced_at)
* feat(whatsapp): hide group management UI when group_left is true
* fix(groupMembers): include inbox phone number in group members state and sync event
* feat(whatsapp): wrap group settings and leave in Accordion component
* feat(groupMembers): handle group creator modification errors and update error messages
* feat(groupMembers): enhance invite link functionality and clean up UI state after copying
* refactor: remove sync_group functionality from conversations and related specs
* feat(GroupContactInfo): implement scroll-based loading for group members
* docs(swagger): add group API endpoints and remove conversation sync_group
- Remove dead conversation/{id}/sync_group swagger entry and file
- Update group_members.yml with pagination params, POST operation, and $ref schema
- Add swagger for: group_members_member (PATCH/DELETE), group_metadata,
group_invite, group_invite_revoke, group_join_requests,
group_join_requests_handle, group_settings, group_settings_leave,
group_settings_toggle_join_approval, groups/create
- Add group_member schema definition
- Add Groups tag to application tag_groups
- Register all 12 group endpoints in paths/index.yml
* feat(WhatsappBaileysService): enhance mention handling by replacing @DisplayName with @lid/@phone in outgoing text
* feat(groups): move group sync to background job with 15-min cooldown
- Create Contacts::SyncGroupJob that checks group_last_synced_at
before calling SyncGroupService (skips if < 15 min)
- Controller sync_group now enqueues the job and returns 202 Accepted
- Delete sync_group.json.jbuilder (no longer needed)
- Frontend sync action is fire-and-forget; results via ActionCable
- Auto-trigger sync on conversation select and panel mount
- Remove manual sync button from GroupContactInfo
* fix: show group members list even after leaving group\n\nKeep the members section visible in read-only mode when\ngroup_left is true. Admin actions (add member, promote,\ndemote, remove) remain hidden. Pending Join Requests and\nAdvanced Options also stay hidden.
* fix: disable group name/description/avatar editing when group_left is true
* fix: remove @all mention and fix Enter key in group mention dropdown\n\n- Remove the @all/everyone special mention from TagGroupMembers since\n no channel provider currently supports mentioning all participants\n- Fix Enter key sending message instead of inserting selected mention\n in group conversations. The root cause was Editor.vue only emitting\n toggleUserMention=true for private notes (isPrivate), leaving\n ReplyBox unaware the group mention dropdown was open. Now also\n emits for isGroupConversation.\n- Add TagGroupMembers spec covering filtering, exclusion, and emission"
* fix: address PR review feedback for group conversations
- Fix nil safety in group_invites and group_join_requests controllers
by replacing group_conversation.inbox.channel with @contact.group_channel
- Add before_action guard in group_members_controller to validate
contact is a group with identifier before create/update/destroy
- Persist metadata locally in group_metadata_controller after
provider calls (subject -> name, description -> additional_attributes)
- Add server-side allow_group_creation? check in groups_controller
- Add word boundary to mention regex to prevent matching inside words
- Remove useless catch clauses in groupMembers store (try/finally only)
- Default groupType to [] in customViewsHelper to prevent crash
- Fix swagger parameter name mismatch (contact_id -> id) across
all group endpoint YML files for consistency
* fix: address PR #228 review feedback - strong params, guards, and safety fixes
* fix: dispatch real-time events for Baileys group participant and metadata updates
Both group-participants.update and groups.update handlers were updating
backend data (GroupMember records, Contact attributes) but never
dispatching ActionCable events, leaving the frontend member list and
group metadata stale until manual sync.
Changes:
- Add dispatch_group_synced_event helper to GroupEventHelper concern
- Dispatch CONTACT_GROUP_SYNCED after participant add/remove/promote/demote
- Dispatch CONTACT_GROUP_SYNCED after group subject/description/settings changes
- Frontend: onContactGroupSynced also dispatches contacts/updateContact
to refresh group name, description and settings in the sidebar
* fix: enhance member menu positioning and close behavior on sidebar scroll
* feat: implement group property updates and enhance toast notifications
* fix: update WhatsApp channel regex to allow optional hyphenated numbers
* feat: implement group admin functionalities including leave, update properties, and toggle join approval
* refactor: simplify group message handling by removing metadata fetching and syncing methods
* chore: remove raph files
* feat: update Portuguese translations for 'Read More' and 'Insert Read More' phrases
* feat: enhance group admin functionalities with join approval and member add modes
* feat: enhance group join request handling by adding removal of handled requests and updating pending join requests
* feat: restrict message sending in announcement mode groups
When a Baileys WhatsApp group has announcement mode enabled (announce=true),
only admin members can send messages. This adds:
- Frontend: disabled editor + banner for non-admin inbox in announcement groups
- Backend: validation in SendOnWhatsappService to reject messages
- Shared phone helper utility extracted from GroupContactInfo
- i18n keys for en and pt_BR
* feat: add group sync job enqueueing and improve avatar update handling
* feat: add functionality to reset invite link and confirm member addition restrictions
* feat: update group name extraction logic to handle nil values
* feat: add inbox admin status handling and update related components
* feat: remove group conversation resolution on leave action
* feat: enhance group sender avatar interaction with tooltip and cursor pointer
* feat: add force option to SyncGroupJob and update related specs
* feat: enhance invite link handling and avatar update logic in group conversations
* chore: remove prd.json
* fix: change group sender name display from block to inline-block for better layout
* feat: add group members loading check and fetch logic in MessagesView and ReplyBox components
* feat: allow id and firstUnreadId props to accept both Number and String types
feat: add vOnClickOutside import to Editor component
feat: enhance Portuguese translations for integrations and settings
fix: change button color in GroupContactInfo component from green to teal
* feat: soft-disabled group conversations with activity tracking
Groups start in a soft-disabled state by default when using Baileys.
Chatwoot still creates group conversations but does not process every
incoming message. Instead, Baileys accumulates group messages and sends
periodic groups.activity webhook events to update last_activity_at.
Backend:
- Add WHATSAPP_GROUPS_ENABLED env var and groups_enabled? class method
- Send groupsEnabled in Baileys connection setup
- Create groups.activity handler to update conversation last_activity_at
- Gate group message processing behind groups_enabled? check
- Expose groups_enabled via inbox API
Frontend:
- Add warning banner with CTA to app.fazer.ai on disabled group conversations
- Disable reply editor for non-private-note mode when groups disabled
- Add i18n strings for en and pt_BR
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use method for groups disabled banner action to avoid window scope issue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: broadcast conversation update after groups.activity event
update_columns bypasses ActiveRecord callbacks, so the ActionCable
broadcast was never triggered when last_activity_at changed. Dispatch
a CONVERSATION_UPDATED event explicitly so the sidebar updates in
real-time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: show unread dot for soft-disabled group conversations with activity
Since soft-disabled groups don't create messages, unread_count is
always 0 and the standard badge won't show. Detect unread state by
comparing last_activity_at > agent_last_seen_at for these groups
and display a teal dot indicator instead of a count badge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clear unread dot when agent opens soft-disabled group conversation
The update_last_seen endpoint skipped updating agent_last_seen_at when
there were no unread messages (the throttle path). For soft-disabled
groups that never create messages, this meant the dot indicator could
never be cleared. Add an unseen_activity? check that bypasses the
throttle when last_activity_at > agent_last_seen_at.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: group avatar upload to provider and fix icon change sync
- Route avatar upload through GroupMetadataController to push to WhatsApp
provider before saving locally
- Add update_group_picture to baileys service and base service
- Fix buildContactFormData crash when social_profiles is undefined
- Make try_update_group_avatar public so GROUP_CHANGE_ICON stub handler
can call it from outside the service class
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update specs for group conversations feature changes
- Add groupsEnabled param to setup_channel_provider and handle_channel_error WebMock stubs
- Add group-request-participants-list stub for sync_group tests
- Add group_type to push_event_data expected hash
- Set last_activity_at in throttle tests to prevent unseen_activity? bypass
- Update sync_group delegation expectation to include soft: false
- Stub groups_enabled? in group message handling tests
- Update WhatsApp source_id regex expectation for group contact IDs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add settings file for additional directories configuration
* chore: undo unrelated changes
* chore: remove planning doc, fix migration version, fix swagger param consistency
- Remove planejamento-chat-interno.md (unrelated planning document)
- Fix CreateGroupMembers migration API version from 7.0 to 7.1
- Fix swagger.json: normalize group endpoint paths from {contact_id} to {id}
to match YAML sources and existing contact sub-resource conventions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CayoPOliveira <cayoproliveira@gmail.com>
Co-authored-by: Cayo P. R. Oliveira <cayo@fazer.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(whatsapp): consolidate fragmented phone/LID contacts to prevent lost messages
When a WhatsApp user had two separate contacts (one by phone, one by LID)
with different contact_ids, the consolidation service did nothing. This caused
update_contact_info to crash with a phone_number uniqueness violation, silently
dropping incoming messages.
Now properly merges the two contacts by treating the phone contact as canonical,
moving conversations from the LID contact, and cleaning up duplicates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): address CodeRabbit review feedback
- Destroy orphaned LID contact when it has no remaining contact_inboxes
- Clarify spec comment to past-tense (no longer an active bug)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): route legacy-source contact_inbox into merge path
When a phone contact_inbox has a non-standard source_id (legacy format)
and a separate LID contact_inbox exists for a different contact,
the consolidation now merges them instead of early-returning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: assert orphaned LID contact is destroyed in legacy-source spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Description
Add missing JSON imports and spread exports for `companies`,
`contentTemplates`, `mfa`, `snooze`, `webhooks`, and `yearInReview` so
these translations are properly loaded in the pt_BR locale. Without
these imports, those sections of the UI were falling back to English for
Brazilian Portuguese users.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## PR#1: Reporting events rollup — model and write path
Reporting queries currently hit the `reporting_events` table directly.
This works, but the table grows linearly with event volume, and
aggregation queries (counts, averages over date ranges) get
progressively slower as accounts age.
This PR introduces a pre-aggregated `reporting_events_rollups` table
that stores daily per-metric, per-dimension (account/agent/inbox)
totals. The write path is intentionally decoupled from the read path —
rollup rows are written inline from the event listener via upsert, and a
backfill service exists to rebuild historical data from raw events.
Nothing reads from this table yet.
The write path activates when an account has a `reporting_timezone` set
(new account setting). The `reporting_events_rollup` feature flag
controls only the future read path, not writes — so rollup data
accumulates silently once timezone is configured. A `MetricRegistry`
maps raw event names to rollup column semantics in one place, keeping
the write and (future) read paths aligned.
### What changed
- Migration for `reporting_events_rollups` with a unique composite index
for upsert
- `ReportingEventsRollup` model
- `reporting_timezone` account setting with IANA timezone validation
- `MetricRegistry` — single source of truth for event-to-metric mappings
- `RollupService` — real-time upsert from event listener
- `BackfillService` — rebuilds rollups for a given account + date from
raw events
- Rake tasks for interactive backfill and timezone setup
- `reporting_events_rollup` feature flag (disabled by default)
### How to test
1. Set a `reporting_timezone` on an account
(`Account.first.update!(reporting_timezone: 'Asia/Kolkata')`)
2. Resolve a conversation or trigger a first response
3. Check `ReportingEventsRollup.where(account_id: ...)` — rows should
appear
4. Run backfill: `bundle exec rake reporting_events_rollup:backfill` and
verify historical data populates
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Devise 4.9.x has a race condition in the reconfirmable flow where
concurrent email change requests can desynchronize the confirmation
token from `unconfirmed_email`, letting an attacker confirm an email
they don't own. We use `:confirmable` with `reconfirmable = true`, so
we're directly exposed.
The upstream fix is in Devise 5.0.3, but we can't upgrade —
`devise-two-factor` only supports Devise 5 from v6.4.0, which also
raised its Rails minimum to 7.2+. No released version supports both
Devise 5 and Rails 7.1.
This PR ports the Devise 5.0.3 fix locally by overriding
`postpone_email_change_until_confirmation_and_regenerate_confirmation_token`
on the User model to persist the record before regenerating the token.
This is a stopgap — remove it once the dependency chain allows upgrading
to Devise 5.
### How to test
Sign in as a confirmed user and change your email. The app should send a
confirmation to the new address while keeping the current email
unchanged until confirmed.
The Platform API was returning the bot's `name` value for the
`outgoing_url` field across all agent bot endpoints (index, show,
create, update). A typo in the `_agent_bot.json.jbuilder` partial used
`resource.name` instead of `resource.outgoing_url`.
Closes#13787
## What changed
- `app/views/platform/api/v1/models/_agent_bot.json.jbuilder`: corrected
`resource.name` → `resource.outgoing_url` on the `outgoing_url` field.
## How to reproduce
1. Create an agent bot via `POST /platform/api/v1/agent_bots` with a
distinct `outgoing_url`.
2. Call `GET /platform/api/v1/agent_bots/:id`.
3. Before this fix the `outgoing_url` in the response equals the bot's
`name`; after the fix it equals the value set at creation.
## Tests
Added `outgoing_url` assertions to the existing GET index and GET show
request specs so the regression is covered.
When `allowed_domains` is configured on a web widget inbox, the server
responds with Content-Security-Policy: frame-ancestors <domains>, which
blocks the widget iframe in mobile app WebViews. This happens because
WebViews load content from file:// or null origins, which cannot match
any domain in the frame-ancestors directive.
This adds a per-inbox toggle — "Enable widget in mobile apps" — that
skips the frame-ancestors header when the request has no valid Origin
(i.e., it comes from a mobile WebView). Web browsers with a real origin
still get domain restrictions enforced as usual.
<img width="2330" height="1490" alt="CleanShot 2026-03-11 at 10 13
01@2x"
src="https://github.com/user-attachments/assets/d9326fac-020d-4ce7-9ced-0c185468c8fc"
/>
Fixes
https://linear.app/chatwoot/issue/CW-6560/widget-is-not-loading-from-iosandroid-widgets
How to test
1. Go to Settings → Inboxes → (Web Widget) → Configuration
2. Set allowed_domains to a specific domain (e.g., *.example.com)
3. Try loading the widget in a mobile app WebView — it should be blocked
4. Enable "Enable widget in mobile apps" checkbox
5. Reload the widget in the WebView — it should now load successfully
6. Verify the widget on a website not in the allowed domains list is
still blocked
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
This change spreads Chatwoot Hub version checks across the day by
scheduling each installation at a stable minute derived from its
installation identifier, instead of having all instances check at the
same fixed time.
Closes
-
https://linear.app/chatwoot/issue/CW-6107/handle-the-spike-at-12-utc-on-chatwoot-hub
What changed
- Added `Internal::TriggerDailyScheduledItemsJob` to act as the daily
trigger for deferred internal jobs.
- Updated the version check cron entry to run once daily at `00:00 UTC`
and enqueue the actual version check for that installation’s assigned
minute of the day.
- Used a deterministic minute-of-day derived from
`ChatwootHub.installation_identifier` so the check time stays stable
across deploys and restarts.
- Kept the existing cron schedule key while switching it to the new
orchestrator job.
How to test
- Run `bundle exec rspec
spec/jobs/internal/check_new_versions_job_spec.rb
spec/jobs/internal/trigger_daily_scheduled_items_job_spec.rb
spec/configs/schedule_spec.rb`
- In a Rails console, run
`Internal::TriggerDailyScheduledItemsJob.perform_now` and verify
`Internal::CheckNewVersionsJob` is enqueued with a `wait_until` later
the same UTC day.
- In Super Admin settings, use Refresh and verify the version check
still runs immediately.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This adds a draft status for Help Center locales so teams can prepare
localized content in the dashboard without exposing those locales in the
public portal switcher until they are ready to publish.
Fixes: https://github.com/chatwoot/chatwoot/issues/10412
Closes: https://github.com/chatwoot/chatwoot/issues/10412
## Why
Teams need a way to work on locale-specific Help Center content ahead of
launch. The public portal should only show ready locales, while the
admin dashboard should continue to expose every allowed locale for
ongoing article and category work.
## What this change does
- Adds `draft_locales` to portal config as a subset of `allowed_locales`
- Hides drafted locales from the public portal language switchers while
keeping direct locale URLs working
- Keeps drafted locales fully visible in the admin dashboard for article
and category management
- Adds locale actions to move an existing locale to draft, publish a
drafted locale, and keep the default locale protected from drafting
- Adds a status dropdown when creating a locale so new locales can be
created as `Published` or `Draft`
- Returns each admin locale with a `draft` flag so the locale UI can
reflect the public visibility state
## Validation
- Seed a portal with multiple locales, draft one locale, and confirm the
public portal switcher hides it while `/hc/:slug/:locale` still loads
directly
- In the admin dashboard, confirm drafted locales still appear in the
locale list and remain selectable for articles and categories
- Create a new locale with `Draft` status and confirm it stays out of
the public switcher until published
- Move an existing locale back and forth between `Published` and `Draft`
and confirm the public switcher updates accordingly
## Demo
https://github.com/user-attachments/assets/ba22dc26-c2e7-463a-b1f5-adf1fda1f9be
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Fixes help center public article search so query responses stay compact
and locale-scoped. Whitespace-only queries are now treated as empty in
both the portal UI and the server-side search path, and search
suggestions stay aligned with the trimmed query.
Fixes: https://github.com/chatwoot/chatwoot/issues/10402
Closes: https://github.com/chatwoot/chatwoot/issues/10402
## Why
The public help center search endpoint reused the full article
serializer for query responses, which returned much more data than the
search suggestions UI needed. That made responses heavier than necessary
and also surfaced nested portal and category data that made the results
look cross-locale.
Whitespace-only searches could also still reach the backend search path,
and in Enterprise that meant embedding search could be invoked for a
blank query.
## What changed
- return a compact search-specific payload for article query responses
- keep the existing full article serializer for normal article listing
responses
- preserve current-locale search behavior for the portal search flow
- trim whitespace-only search terms on the client so they do not open
suggestions or trigger a request
- reuse the normalized query on the backend so whitespace-only requests
are treated as empty searches in both OSS and Enterprise paths
- pass the trimmed search term into suggestions so highlighting matches
the actual query being sent
- add request and frontend regression coverage for compact payloads,
locale scoping, and whitespace-only search behavior
## Validation
1. Open `/hc/:portal/:locale` in the public help center.
2. Enter only spaces in the search box and confirm suggestions do not
open.
3. Search for a real term and confirm suggestions appear.
4. Verify the results are limited to the active locale.
5. Click a suggestion and confirm it opens the correct article page.
6. Inspect the query response and confirm it returns the compact search
payload instead of the full article serializer.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Email inbox replies now work for Google and Microsoft OAuth inboxes even
when the self-hosted instance does not have global SMTP configured. This
keeps agent replies working for email channels that already have valid
inbox-level delivery settings.
fixes: chatwoot/chatwoot#13118closes: chatwoot/chatwoot#13118
## Why
Self-hosted email inbox replies were blocked by a global SMTP guard in
the `email_reply` path. For OAuth-backed email inboxes, outbound
delivery is configured at the inbox level, so the mailer returned early
and the reply flow failed before sending.
## What this change does
- Allows the `email_reply` path to proceed when the inbox has SMTP
configured
- Allows the `email_reply` path to proceed when the inbox has Google or
Microsoft OAuth delivery configured
- Renames the touched mailer helper predicates to `?` methods for
clarity
## Validation
- Configure a Google email inbox on a self-hosted instance without
global `SMTP_ADDRESS`
- Reply from Chatwoot to an existing email conversation
- Confirm the reply is sent through the inbox OAuth SMTP configuration
- Run `bundle exec rspec
spec/mailers/conversation_reply_mailer_spec.rb:595`
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
This PR improves the Ukrainian translation for the Chatwoot widget
(`app/javascript/widget/i18n/locale/uk.json`).
Key changes:
- Fixed typo: `Звантажити` → `Завантажити`
- Translated missing English strings
- Improved reply time messages
- Updated day names to match `{day}` usage in `BACK_ON_DAY`
- Improved UX wording in form placeholders
- Fixed typography in `ім’я`
- Improved consistency with other Chatwoot translations
These updates improve readability and correctness of the Ukrainian
widget interface.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Reviewed the updated translations and verified that:
- Ukrainian translations render correctly
- Reply time messages display properly
- `{day}` values work correctly with the `BACK_ON_DAY` message
- Form placeholders appear correctly
- No untranslated English strings remain
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This updates the Traditional Chinese (`zh_TW`) locale coverage across
Chatwoot so the app no longer falls back to English for missing backend,
dashboard, widget, and survey strings.
## How to test
1. Start Chatwoot locally and switch the UI locale to Traditional
Chinese (`zh_TW`).
2. Walk through the main product areas: dashboard, settings, inbox
management, help center, automations, reports, widget, and survey flows.
3. Confirm the UI surfaces translated Traditional Chinese copy instead
of English fallbacks.
4. Spot-check newly added locale surfaces such as secure password
messaging and snooze UI copy.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Translate all English strings to Korean across 41 frontend locale files
and 2 backend locale files. Add structurally missing keys and translate
existing keys that were left in English.
# Pull Request Template
## Description
Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Sojan Jose <sojan@pepalo.com>
# Pull Request Template
## Description
Updated few i18n files to:
1. fix typos / grammar / punctuation
2. translate strings that were still in english
3. add missing keys
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
i18n change, the format remained the same.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Description
Fixes the reversed message delivery status indicators for the API
channel. The API inbox was grouped with the web widget inbox in the
`isDelivered` computed property, causing both to treat a `sent` status
as `delivered`. Since the API channel provides real
`sent`/`delivered`/`read` status values from external systems (unlike
the web widget which has no separate delivery confirmation), the API
inbox needs its own handling.
**Before this fix:**
- Status `sent` (0) → incorrectly showed delivered checkmarks
- Status `delivered` (1) → incorrectly showed "Sending" spinner
**After this fix:**
- Status `sent` → correctly shows sent indicator (single checkmark)
- Status `delivered` → correctly shows delivered indicator (double
checkmarks)
- Status `read` → unchanged (already worked correctly)
The web widget inbox behavior is unchanged — it still treats `sent` as
`delivered` since it lacks a separate delivery confirmation mechanism.
Fixes#13576
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Verified by code review that the computed properties now correctly map
API channel message statuses:
- `isSent` returns `true` when `status === 'sent'` for API inbox
- `isDelivered` returns `true` when `status === 'delivered'` for API
inbox
- `isRead` unchanged — already checks `status === 'read'` for API inbox
- Web widget inbox logic is unchanged
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings
*This PR was created with the assistance of Claude Opus 4.6 by
Anthropic. Happy to make any adjustments! Reviewed and submitted by a
human.*
Co-authored-by: Your Name <your-email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
When an agent resolves or snoozes a conversation, their capacity frees
up — but under Assignment V2, no new conversation was assigned until the
next event triggered the assignment job. This meant agents could sit
idle despite having queued conversations waiting. This change triggers
the AssignmentJob immediately on resolve/snooze so freed capacity is
utilized right away.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
- Enable auto assignment v2 on an inbox
- Create a conversation (gets auto-assigned to an available agent)
- Resolve the conversation
- Verify that another unassigned conversation in the inbox gets assigned
to the freed-up agent
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: tds-1 <tds-1@users.noreply.github.com>
## Description
Adds webhook configuration management for WhatsApp Cloud API channels,
allowing administrators to check webhook status and register webhooks
directly from Chatwoot without accessing Meta Business Manager.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## Screenshots
<img width="1130" height="676" alt="Screenshot 2026-03-05 at 7 04 18 PM"
src="https://github.com/user-attachments/assets/f5dcd9dd-8827-42c5-a52b-1024012703c2"
/>
<img width="1101" height="651" alt="Screenshot 2026-03-05 at 7 04 29 PM"
src="https://github.com/user-attachments/assets/e0bd59f9-2a90-4f24-87c0-b79f21e721ee"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
Add account setting and store_accessor for
`captain_force_legacy_auto_resolve`.
Enterprise job now skips LLM evaluation when this flag is true and falls
back to legacy time-based resolution. Add spec to cover the fallback.
## Type of change
We recently rolled out Captain deciding if a conversation is resolved or
not. While it is an improvement for majority of customers, some still
prefer the old way of auto-resolving based on inactivity. This PR adds a
check.
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
legacy_auto_resolve = true
<img width="1282" height="848" alt="CleanShot 2026-03-13 at 19 55 55@2x"
src="https://github.com/user-attachments/assets/dfdcc5d5-6d21-462b-87a6-a5e1b1290a8b"
/>
legacy_auto_resolve = false
<img width="1268" height="864" alt="CleanShot 2026-03-13 at 20 00 50@2x"
src="https://github.com/user-attachments/assets/f4719ec6-922a-4c3b-bc45-7b29eaced565"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template
## Description
CJK language users (Chinese, Japanese, Korean, etc.) use IME where Enter
confirms character selection. AI input components were intercepting
Enter unconditionally, making them unusable for IME users.
Add `event.isComposing` check to CopilotEditor, CopilotInput, and
AssistantPlayground so Enter during active IME composition is ignored.
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Before:
Add a Japenese keyboard, then go to AI follow-ups, type some word,
selecting it with enter submits the follow up. So CJK users cannot use
follow-ups.
https://github.com/user-attachments/assets/53517432-d97b-47fc-a802-81675e31d5c9
After:
Type a word, press enter to choose it, press enter again to unselect it
and enter again to send
https://github.com/user-attachments/assets/6c2a420b-7ee6-4c71-82a6-d9f1d7bbf31a
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
# Pull Request Template
## Description
captain decides if conversation should be resolved or open
Fixes
https://linear.app/chatwoot/issue/AI-91/make-captain-resolution-time-configurable
Update: Added 2 entries in reporting events:
`conversation_captain_handoff` and `conversation_captain_resolved`
## Type of change
Please delete options that are not relevant.
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
LLM call decides that conversation is resolved, drops a private note
<img width="1228" height="438" alt="image"
src="https://github.com/user-attachments/assets/fb2cf1e9-4b2b-458b-a1e2-45c53d6a0158"
/>
LLM call decides conversation is still open as query was not resolved
<img width="1215" height="573" alt="image"
src="https://github.com/user-attachments/assets/2d1d5322-f567-487e-954e-11ab0798d11c"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
* feat: add kanban feature promotion with upgrade banner
Add a Kanban sidebar item visible to all users that shows a locked
feature promotion page. Super admins see an upgrade button linking to
fazer.ai, while non-admins see a message to contact their administrator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: allow custom_role users to access kanban promotion page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the SDK sends identify calls with identical payloads (common on
every page load), `save!` fires even though no attributes changed. While
Rails skips the actual UPDATE SQL, it still opens a transaction, runs
all callbacks (including validation queries like `Contact Exists?`), and
triggers `after_commit` hooks — all for a no-op.
This adds a `changed?` guard before `save!` to skip it entirely when no
attributes have actually changed.
**How to test**
- Trigger an identify call via the SDK with a contact's existing
attributes (same name, email, custom_attributes, etc.)
- The contact should not fire a save (no transaction, no callbacks)
- Trigger an identify call with a changed attribute — save should work
normally
**What changed**
- `ContactIdentifyAction#update_contact`: guard `save!` with `changed?`
check
- Added specs to verify `save!` is skipped for unchanged params and
avatar job still enqueues independently
Removes touch: true from the belongs_to :conversation association on
Message and consolidates the conversation timestamp update into the
existing set_conversation_activity callback.
Previously, every message save triggered two separate UPDATE queries on
the conversation — one from Rails' touch (updating updated_at) and
another from set_conversation_activity (updating last_activity_at). This
combines both into a single update_columns call, reducing write load on
the conversations table on every message creation.
### What changed
- Removed touch: true from belongs_to :conversation in Message
- Added updated_at: created_at to the existing update_columns call in
set_conversation_activity
# Pull Request Template
## Description
Unconfirmed agents (pending email verification) were incorrectly
appearing in the "assign agent" dropdown for macros and automations.
This fix filters out unconfirmed agents from these dropdowns and adds
backend validation to prevent assignment of unconfirmed agents.
Fixes#13223
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Backend tests:**
```bash
docker compose run --rm rails bundle exec rspec spec/services/action_service_spec.rb
```
- Added tests for confirmed agent assignment (should succeed)
- Added tests for unconfirmed agent assignment (should be skipped)
**Frontend tests:**
```bash
docker compose run --rm rails pnpm test app/javascript/dashboard/composables/spec/useMacros.spec.js
```
- Updated mocks to use `getVerifiedAgents` getter
**Manual testing:**
1. Create an unconfirmed agent via platform
2. Navigate to Settings → Macros → New Macro → Add "Assign Agent" action
3. Verify unconfirmed agent does NOT appear in dropdown
4. Navigate to Settings → Automations → New Automation → Add "Assign
Agent" action
5. Verify unconfirmed agent does NOT appear in dropdown
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
# Pull Request Template
## Description
Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## Description
This PR optimizes message queries by explicitly filtering with
`account_id` so the database can use the existing indexes more
efficiently.
Changes:
- Add `account_id` to message query filters to improve index
utilization.
- Update `last_incoming_message` query to include `account_id`.
- Avoid unnecessary preloading of `contact_inboxes` where it is not
required.
- Update specs to ensure `account_id` is set correctly in
message-related tests.
These changes reduce query cost and improve performance for message
lookups, especially on large accounts.
---------
Co-authored-by: Pranav <pranav@chatwoot.com>
This makes account signup enforcement consistent when signup is disabled
at the installation level. Email signup and Google signup now stay
blocked regardless of whether the config value is stored as a string or
a boolean.
This effectively covers the config-loader path, where `YAML.safe_load`
reads `value: false` from `installation_config.yml` as a native boolean
and persists it that way.
- Normalized the account signup check so disabled signup is handled
consistently across config value types.
- Reused the same check across API signup and Google signup entry
points.
- Added regression coverage for the disabled-signup cases in the
existing controller specs.
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
- Extracted 6 custom attribute methods (`custom_attribute_query`,
`attribute_model`, `attribute_data_type`, `build_custom_attr_query`,
`custom_attribute`, `not_in_custom_attr_query`) into a new
`Filters::CustomAttributeFilterHelper` module.
- Added an inline `rubocop:disable` for the intentional
`Lint/ShadowedException` in `coerce_lt_gt_value` — `Date::Error` is a
subclass of `ArgumentError`, but both are listed explicitly for clarity.
## Why `app/services/filters/`
The existing `Filters::FilterHelper` lives in `app/helpers/filters/`,
but that location triggers `Rails/HelperInstanceVariable` for any module
that uses instance variables. The extracted methods share state with
`FilterService` via instance variables (`@attribute_key`, `@account`,
`@custom_attribute`, etc.), so placing them in `app/helpers/` would
require a cop disable.
`app/services/filters/` is a better fit because:
- The module is a service mixin, not a view helper — it's only included
by `FilterService` and its subclasses (`Conversations::FilterService`,
`Contacts::FilterService`, `AutomationRules::ConditionsFilterService`).
- It sits alongside the services that use it.
- No cop disables needed.
---------
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
On self-hosted instances without email configured, users created from
Super Admin can get stuck in an unconfirmed state. This PR implements
the default at the Super Admin frontend form layer, not in backend
creation logic.
What changed:
- Added a custom `ConfirmedAtField` for Super Admin user forms.
- Prefills `confirmed_at` with current time on the **New User** form
(`GET /super_admin/users/new`).
- Kept backend create behavior unchanged
(`resource_class.new(resource_params)`), so API/manual payloads still
behave normally.
Behavior:
- In Super Admin UI, `confirmed_at` is prefilled by default.
- If someone wants an unconfirmed user, they can clear the
`confirmed_at` field before saving.
- If `confirmed_at` is omitted from payload entirely, the created user
remains unconfirmed.
Scope note: external signup flows are intentionally unchanged in this PR
(`/api/v1/accounts`, `/api/v2/accounts`, and social/omniauth signup
behavior are not modified).
## Demo
https://github.com/user-attachments/assets/436abbb0-d4cf-49a6-a1b8-4b6aa85aa09f
## Description
Reduces the frequency of update_presence WebSocket calls from the live
chat widget and fixes agents appearing offline when the dashboard is in
a background tab.
## Fixes # (issue)
https://github.com/chatwoot/chatwoot/issues/13720
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Agents using API channel inboxes (e.g., WhatsApp Automate) reported
seeing the same conversation appear twice in their conversation list —
one showing the last message preview and the other showing "No
Messages". Backend investigation confirmed no duplicate conversations
exist in the database, making this purely a frontend issue.
The root cause is a race condition in WebSocket event delivery. When a
conversation is created via the API with auto-assignment, the backend
enqueues multiple ActionCable broadcast jobs (`conversation.created`,
`assignee.changed`, `team.changed`) within milliseconds of each other.
In production with multi-threaded Sidekiq workers, these events can
arrive at the frontend out of order. If `assignee.changed` arrives
before `conversation.created`, the `UPDATE_CONVERSATION` mutation pushes
the conversation into the store (since it doesn't exist yet), and then
`ADD_CONVERSATION` blindly pushes it again — resulting in a duplicate
entry.
The fix adds a uniqueness check in the `ADD_CONVERSATION` mutation to
skip the push if a conversation with the same ID already exists in the
store, matching the dedup pattern already used by
`SET_ALL_CONVERSATION`.
Fixes https://linear.app/chatwoot/issue/PLA-96/argumenterror-reel-is-not-a-valid-file-type-argumenterror
Fixes a crash in `when a user shares a Facebook reel via Messenger.
Facebook sends these attachments with `type: "reel"`, which isn't a
valid `file_type` enum value on the Attachment model (only `ig_reel`
exists, matching Instagram's format).
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>