Compare commits

...
Author SHA1 Message Date
Shivam Mishra 5b72f3a26b feat: always print build SHA 2026-06-08 17:47:41 +05:30
Shivam Mishra 70e9fbc53c refactor: catch failures 2026-06-04 13:06:54 +05:30
Shivam Mishra 186d9e0457 feat: fetch teams and agents 2026-06-04 13:06:22 +05:30
Shivam Mishra 295bc94fa3 feat: use single inbox id 2026-05-14 20:30:02 +05:30
Shivam Mishra 83e96956fc feat: remove usage of .last 2026-04-23 14:44:52 +05:30
Shivam Mishra 190416d765 test: handle setActiveChat 2026-04-23 12:55:46 +05:30
Shivam Mishra 3bf370dbba chore: remove isolated shell check 2026-04-23 11:47:41 +05:30
Shivam Mishra 5b4d3c1f83 fix: setactivechat last message 2026-04-22 19:05:26 +05:30
iamsivin 475ca365c0 feat: add context menu disable feature 2026-04-21 18:23:05 +05:30
Shivam Mishra 6ff0c67501 fix: useInbox 2026-04-08 21:26:47 +05:30
Shivam Mishra 3bd4570580 feat: move inboxes get to provider 2026-04-08 20:53:08 +05:30
Shivam Mishra fb75f8783f fix: remove multiselect 2026-03-20 17:38:47 +05:30
Shivam Mishra 135357a455 fix: merge conflict issues 2026-03-20 17:35:47 +05:30
Shivam Mishra 5670f38eb4 Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib 2026-03-20 17:34:27 +05:30
Sivin VargheseandGitHub 251e9980fd chore: Auto-focus editor when replying to a message (#13857)
# 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
2026-03-20 16:59:27 +05:30
Tanmay Deep SharmaandGitHub 2b50909d9b fix: use last_activity_at for orphan conversation cleanup timeframe (#13859)
## 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
2026-03-20 16:28:05 +05:30
Aakash BakhleandGitHub 290dd3abf5 feat: allow captain to access contact attributes (#13850)
# 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
2026-03-20 16:15:06 +05:30
Shivam Mishra 477dad1e8e fix: persistance 2026-03-20 13:32:07 +05:30
a9123e7d66 chore(i18n): add missing pt_BR locale imports for companies, mfa, snooze, webhooks and more (#13844)
## 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>
2026-03-19 01:27:37 -07:00
Shivam Mishra 6b1389a489 fix: new line 2026-03-19 13:39:18 +05:30
Shivam Mishra b19747a23a fix: correct format for signature 2026-03-19 13:35:18 +05:30
Shivam Mishra 64d0aae672 fix: disable IDB cache 2026-03-19 13:31:16 +05:30
9967101b48 feat(rollup): add models and write path [1/3] (#13796)
## 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>
2026-03-19 13:12:36 +05:30
Shivam Mishra 6d3926413b fix: height 2026-03-19 13:10:01 +05:30
Shivam Mishra 3489e21753 fix: signature reaonly toggle 2026-03-19 13:08:24 +05:30
654fcd43f2 docs(swagger): fix public API schema definitions to match jbuilder responses (#13693)
## Description

This PR updates the OpenAPI schema definitions for Public API resources
(`public_conversation`, `public_message`, `public_contact`) so they
match the actual API responses produced by the jbuilder views.

These definitions were introduced in #2417 (2021-06) with a minimal set
of fields. The jbuilder views have since been updated (e.g. `uuid` in
#7255, `agent_last_seen_at` in #4377), but the Swagger definitions were
never updated. As a result, generated API clients get incorrect or
missing types. This change fixes that by aligning the schemas with the
implementation.

**Fixes #13692**

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [x] This change requires a documentation update

## How Has This Been Tested?

- Compared each jbuilder view
(`app/views/public/api/v1/models/_conversation.json.jbuilder`,
`_message.json.jbuilder`, `_contact.json.jbuilder`) field-by-field
against the Swagger YAML definitions.
- Cross-referenced Ruby model enums (`Conversation.status`,
`Attachment.file_type`, `Message.message_type`) for enum values.
- Ran the swagger build (via the project’s `rake swagger:build` logic /
`json_refs` resolution) to regenerate `swagger.json` and tag group
files; confirmed the generated schemas contain the correct fields and
types.
- No runtime tests were run; this is a documentation/schema-only change.

## 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 *(N/A: schema definitions)*
- [x] 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 *(N/A: documentation/schema only)*
- [ ] New and existing unit tests pass locally with my changes *(N/A: no
code under test)*
- [x] Any dependent changes have been merged and published in downstream
modules *(N/A: none)*

---

## Change summary (for reference)

### `public_conversation`
- Added missing fields: `uuid`, `status`, `contact_last_seen_at`,
`agent_last_seen_at`
- Fixed `inbox_id` type from `string` to `integer`
- Fixed `messages` items `$ref` from `message` to `public_message`
- Added property details for embedded `contact` object (`id`, `name`,
`email`, `phone_number`)
- Added `status` enum: `open`, `resolved`, `pending`, `snoozed`

### `public_message`
- Fixed `id`, `message_type`, `created_at`, `conversation_id` types
(string → integer where applicable)
- Fixed `content_attributes` type from `string` to `object`
- Added property details for `attachments` items and `sender` object
- Added `file_type` and `sender.type` enums

### `public_contact`
- Added missing `phone_number` field

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-19 00:03:37 -07:00
Shivam Mishra 6d541a8d39 feat: force read-only signature 2026-03-19 10:46:22 +05:30
Shivam Mishra 07b9dbc720 feat: force custom signature 2026-03-19 10:31:36 +05:30
Shivam MishraandGitHub 284977687c fix: patch Devise confirmable race condition vulnerability (#13843)
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.
2026-03-18 21:30:09 -07:00
Sojan Jose 18dc77aa56 Merge branch 'release/4.12.0' into develop 2026-03-17 16:23:16 -07:00
Sojan Jose 8aad8ad38e Bump version to 4.12.0 2026-03-17 16:19:39 -07:00
098f7a77b6 chore: Update translations (#13832)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-17 16:02:40 -07:00
msaleh-313andGitHub 9c22d791c4 fix: return correct outgoing_url in Platform agent bot API responses (#13827)
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.
2026-03-17 13:40:45 -07:00
Tanmay Deep SharmaandGitHub 4d344a47dc chore(tds-1): rake task for assignment v2 migration (#13828) 2026-03-17 20:35:03 +05:30
Aakash BakhleandGitHub 38dbda9378 fix: reverse order of api_key for bg task (#13826)
# Pull Request Template

## Description

we were getting 403, 401 errors on `translate_query` on langfuse and
sentry
This happened because, we use the customer's openai key if they have
BYOK
But translation is something they never opt in so we should not use
their quota for it.
This PR addresses the issue.

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

locally

## 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
2026-03-17 17:36:40 +05:30
a4c3d3d8c0 feat(widget): Allow widget loading in mobile app WebViews when domain restrictions are set (#13763)
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>
2026-03-17 14:29:41 +04:00
688218de0a feat: distributed scheduling for version check job (#13042)
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>
2026-03-17 02:27:49 -07:00
Muhsin KelothandGitHub a8d53a6df4 feat(linear): Support refresh tokens and migrate legacy OAuth tokens (#13721)
Linear is deprecating long-lived OAuth2 access tokens (valid for 10
years) in favor of short-lived access tokens with refresh tokens.
Starting October 1, 2025, all new OAuth2 apps will default to refresh
tokens. Linear will no longer issue long-lived access tokens. Please
read more details
[here](https://linear.app/developers/oauth-2-0-authentication#migrate-to-using-refresh-tokens)
We currently use long-lived tokens in our Linear integration (valid for
up to 10 years). To remain compatible, this PR ensures compatibility by
supporting refresh-token-based auth and migrating existing legacy
tokens.

Fixes
https://linear.app/chatwoot/issue/CW-5541/migrate-linear-oauth2-integration-to-support-refresh-tokens
2026-03-17 13:09:03 +04:00
2a90652f05 feat: Add draft status for help center locales (#13768)
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>
2026-03-17 12:45:54 +04:00
270f3c6a80 fix: slim help center search results (#13761)
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>
2026-03-17 00:46:23 -07:00
ad1539c6cf fix(email): Allow inbox OAuth replies without global SMTP (#13820)
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#13118
closes: 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>
2026-03-17 11:10:42 +04:00
349f55b558 fix: upgrade rollup to 4.59.0 to remediate CVE-2026-27606 (#13781)
https://linear.app/chatwoot/issue/CW-6595/vanta-remediate-high-vulnerabilities-identified-in-packages-are

## Description

- Added "rollup": ">=4.59.0" to pnpm.overrides in package.json
- This bumps rollup from 4.52.5 to 4.59.0 (the transitive dep via vite)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Overall Sanity via UI.

## 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>
2026-03-16 23:12:04 -07:00
ef91b8bb42 fix(i18n): improve Ukrainian widget translation (#13819)
## 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>
2026-03-16 22:38:08 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Shivam MishraSojan Jose
de4c837885 chore(deps): bump dompurify from 3.2.4 to 3.3.2 (#13738)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.2.4 to
3.3.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.3.2</h2>
<ul>
<li>Fixed a possible bypass caused by jsdom's faulty raw-text tag
parsing, thanks multiple reporters</li>
<li>Fixed a prototype pollution issue when working with custom elements,
thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Fixed a lenient config parsing in <code>_isValidAttribute</code>,
thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Bumped and removed several dependencies, thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Fixed the test suite after bumping dependencies, thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
</ul>
<h2>DOMPurify 3.3.1</h2>
<ul>
<li>Updated <code>ADD_FORBID_CONTENTS</code> setting to extend default
list, thanks <a
href="https://github.com/MariusRumpf"><code>@​MariusRumpf</code></a></li>
<li>Updated the ESM import syntax to be more correct, thanks <a
href="https://github.com/binhpv"><code>@​binhpv</code></a></li>
</ul>
<h2>DOMPurify 3.3.0</h2>
<ul>
<li>Added the SVG <code>mask-type</code> attribute to default
allow-list, thanks <a
href="https://github.com/prasadrajandran"><code>@​prasadrajandran</code></a></li>
<li>Added support for <code>ADD_ATTR</code> and <code>ADD_TAGS</code> to
accept functions, thanks <a
href="https://github.com/nelstrom"><code>@​nelstrom</code></a></li>
<li>Fixed an issue with the <code>slot</code> element being in both SVG
and HTML allow-list, thanks <a
href="https://github.com/Wim-Valgaeren"><code>@​Wim-Valgaeren</code></a></li>
</ul>
<h2>DOMPurify 3.2.7</h2>
<ul>
<li>Added new attributes and elements to default allow-list, thanks <a
href="https://github.com/elrion018"><code>@​elrion018</code></a></li>
<li>Added <code>tagName</code> parameter to custom element
<code>attributeNameCheck</code>, thanks <a
href="https://github.com/nelstrom"><code>@​nelstrom</code></a></li>
<li>Added better check for animated <code>href</code> attributes, thanks
<a href="https://github.com/llamakko"><code>@​llamakko</code></a></li>
<li>Updated and improved the bundled types, thanks <a
href="https://github.com/ssi02014"><code>@​ssi02014</code></a></li>
<li>Updated several tests to better align with new browser encoding
behaviors</li>
<li>Improved the handling of potentially risky content inside CDATA
elements, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
<li>Improved the regular expression for raw-text elements to cover
textareas, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
</ul>
<h2>DOMPurify 3.2.6</h2>
<ul>
<li>Fixed several typos and removed clutter from our documentation,
thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Added <code>matrix:</code> as an allowed URI scheme, thanks <a
href="https://github.com/kleinesfilmroellchen"><code>@​kleinesfilmroellchen</code></a></li>
<li>Added better config hardening against prototype pollution, thanks <a
href="https://github.com/EffectRenan"><code>@​EffectRenan</code></a></li>
<li>Added better handling of attribute removal, thanks <a
href="https://github.com/michalnieruchalski-tiugo"><code>@​michalnieruchalski-tiugo</code></a></li>
<li>Added better configuration for aggressive mXSS scrubbing behavior,
thanks <a
href="https://github.com/BryanValverdeU"><code>@​BryanValverdeU</code></a></li>
<li>Removed the script that caused the fake entry <a
href="https://security.snyk.io/vuln/SNYK-JS-DOMPURIFY-10176060">CVE-2025-48050</a></li>
</ul>
<h2>DOMPurify 3.2.5</h2>
<ul>
<li>Added a check to the mXSS detection regex to be more strict, thanks
<a
href="https://github.com/masatokinugawa"><code>@​masatokinugawa</code></a></li>
<li>Added ESM type imports in source, removes patch function, thanks <a
href="https://github.com/donmccurdy"><code>@​donmccurdy</code></a></li>
<li>Added script to verify various TypeScript configurations, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Added more modern browsers to the Karma launchers list</li>
<li>Added Node 23.x to tested runtimes, removed Node 17.x</li>
<li>Fixed the generation of source maps, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed an unexpected behavior with <code>ALLOWED_URI_REGEXP</code>
using the 'g' flag, thanks <a
href="https://github.com/hhk-png"><code>@​hhk-png</code></a></li>
<li>Fixed a few typos in the README file</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5e56114cb24079ce52dbc51f76e494b77afa5153"><code>5e56114</code></a>
Getting 3.x branch ready for 3.3.2 release (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1208">#1208</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/e8c95f4a27aa8b041f92b59ab7685a94f7be6208"><code>e8c95f4</code></a>
fix: Fixed the broken package-lock.json</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/9636037c145b769dad0b52da8313301cbf867f46"><code>9636037</code></a>
Update package-lock.json</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5cad4cecf2e647ac66eed25bc02a2415f00dbc8b"><code>5cad4ce</code></a>
Getting 3.x branch ready for 3.3.2 releas (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1205">#1205</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/6fc446a589ab3d1d72ae2a5b71167ba38dbd3096"><code>6fc446a</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1175">#1175</a>
from cure53/main</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/3b3bf917d2b39460de6d130acebdc9243cf3e6ae"><code>3b3bf91</code></a>
Merge branch 'main' of github.com:cure53/DOMPurify</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/9863f4195bae6048de9eb2802219218c6904066c"><code>9863f41</code></a>
chore: Preparing 3.3.1 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/b4e02954dc4172c3944a755f3e99fbb76be64f7b"><code>b4e0295</code></a>
chore: Preparing 3.3.0 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/077746bb2cfb77836dfb628dca7ffc7ced8a5356"><code>077746b</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1170">#1170</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/4de68bba9aba43dc3bba9348df603b64fc06d591"><code>4de68bb</code></a>
build(deps): bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1171">#1171</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.2.4...3.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.2.4&new-version=3.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 21:54:32 -07:00
a62beffeef fix(i18n): complete zh_TW locale coverage (#13792)
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>
2026-03-16 18:47:22 -07:00
Denis PetrovandGitHub b8f6fe5bb7 feat: Bulgarian locale updates(#13635)
Bulgarian locale updates
2026-03-16 17:47:25 -07:00
b866886b55 feat(i18n): complete Korean (ko) translations to 100% coverage (#13583)
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>
2026-03-16 16:54:26 -07:00
b88236e86e chore(i18n): update Russian translations (#13405)
# 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>
2026-03-16 16:10:16 -07:00
11ee741716 chore: Update translations (#13227)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 15:44:32 -07:00
Aakash BakhleandGitHub ac93290c9a fix: skip captain auto-open for templates (#13802)
# Pull Request Template

## Description

WhatsApp template messages are treated as outgoing human messages so
conversations are automatically marked Open, preventing Captain from
responding. #13673

This PR fixes the behaviour for template messages, so that captain can
respond to them.

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


## 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
2026-03-16 18:54:12 +05:30
03719cede0 fix: Correct reversed message status indicators for API channel (#13594)
## 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>
2026-03-16 13:21:18 +04:00
a5c50354fc feat: trigger assignment on resolve (#13780)
## 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>
2026-03-16 13:13:37 +05:30
a452ce9e84 feat(whatsapp): add webhook registration and status endpoints (#13551)
## 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>
2026-03-16 12:48:16 +05:30
Tanmay Deep SharmaandGitHub 28bf9fa5f9 fix: upgrade markdown-it to 14.1.1 to remediate CVE-2026-2327 (#13782)
## Linear tickets
-
https://linear.app/chatwoot/issue/CW-6607/vanta-remediate-medium-vulnerabilities-identified-in-packages-are
-
https://linear.app/chatwoot/issue/CW-6612/vanta-remediate-medium-vulnerabilities-identified-in-packages-are

## Description

Upgrades markdown-it from 13.0.2 to 14.1.1 to remediate CVE-2026-2327

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Sanity testing of golden flows via UI

## 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
2026-03-16 11:11:19 +05:30
Shivam MishraandGitHub 73a90f2841 feat: update bunny video support in HC (#13815)
Bunny Video has added a new URL player.mediadelivery.net, this PR adds
support for the new URL
2026-03-16 11:04:27 +05:30
Aakash BakhleandGitHub a90ffe6264 feat: Add force legacy auto-resolve flag (#13804)
# 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
2026-03-13 15:04:58 -07:00
Alexander UdovichenkoandGitHub 412b72db7c fix: Delete double hmac check (#12464)
## Description

When hmac identity check is enabled according to
[this](https://www.chatwoot.com/hc/user-guide/articles/1677587479-how-to-enable-identity-validation-in-chatwoot)
I found out, that it checked twice.

If `should_verify_hmac? -> true` then hmac checked in `before_action`
and we don't need to do it again later.

This perfomance related and PR fixes this.
2026-03-13 02:30:17 -07:00
Aakash BakhleandGitHub 8aa49f69d2 fix: prefer system API key for completion service (#13799)
# Pull Request Template

## Description

Add an api_key override so internal conversation completions prefers
using the system API key and do not consume customer OpenAI credits.
## 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.

specs and locally

## 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
2026-03-13 13:10:10 +05:30
Shivam MishraandGitHub 550b408656 fix: restrict existing user sign-in to account members (#13793)
SAML sign-in now only links an existing user when that user already
belongs to the account that initiated SSO. New users can still be
created for SAML-enabled accounts, and invited members can continue to
sign in through their IdP, but SAML will no longer auto-attach an
unrelated existing user record during login.

**What changed**
- Added an account-membership check before SAML reuses an existing user
by email.
- Kept first-time SAML user creation unchanged for valid new users.
- Added builder and request specs covering the allowed and rejected
login paths.
2026-03-13 12:22:25 +05:30
b103747584 fix: skip Enter key submission during IME composition in AI inputs (#13779)
# 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>
2026-03-13 10:26:51 +05:30
d6d38cdd7d feat: captain decides if conversation should be resolved or kept open (#13336)
# 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>
2026-03-13 10:03:58 +05:30
Shivam Mishra 7331b0a98d chore: add todo 2026-03-11 14:36:11 +05:30
Shivam Mishra 38d991f19a fix: signature toggle 2026-03-11 14:33:36 +05:30
Shivam Mishra 2f46cf162d feat: allow disabling signature 2026-03-05 16:01:03 +05:30
Shivam Mishra 6a6c3b7c27 fix: mark message read 2026-03-05 15:50:37 +05:30
Shivam Mishra 15c710ec01 feat: allow custom signature 2026-02-26 20:31:15 +05:30
Shivam Mishra 787c99bfd2 fix: useAccount for isolated shell 2026-02-09 16:57:26 +05:30
Shivam MishraandGitHub 76b1769731 Merge branch 'develop' into feat/ui-lib 2026-02-09 16:51:26 +05:30
Shivam Mishra ce5b7e310e fix: h-full 2026-02-04 17:36:12 +05:30
Shivam Mishra 4a2b64cc7c fix: icons 2026-02-04 14:32:14 +05:30
Shivam Mishra 28cc377a8d fix: additional colors 2026-01-16 13:28:56 +05:30
Shivam Mishra ae76548f8e fix: email padding 2026-01-16 13:28:01 +05:30
Shivam Mishra 87e8c50ee1 feat: setup overrides for error and email 2026-01-16 13:24:50 +05:30
Shivam Mishra 8cacf95aae fix: signature not applied 2026-01-15 13:23:03 +05:30
Shivam Mishra e7ae724488 feat: define props 2025-12-22 21:13:25 +05:30
Shivam Mishra 05da860aca feat: force align left 2025-12-22 21:02:29 +05:30
Shivam Mishra 290c2ff4c5 feat: add bubble border 2025-12-22 21:02:07 +05:30
Shivam Mishra 9d95f73ec2 feat: add alignment changes 2025-12-22 20:06:45 +05:30
Shivam Mishra 3396087047 feat: always show avatar when force aligned 2025-12-22 19:47:01 +05:30
Shivam Mishra f6bef53252 feat: add force align to message component 2025-12-22 19:45:35 +05:30
Shivam Mishra c22f8a07c5 chore: revert padding changes 2025-12-22 19:42:36 +05:30
Shivam Mishra e8157a2c0e fix: spacing ratio 2025-12-22 19:36:06 +05:30
Shivam Mishra 7a8b1700ea fix: bubble styles for email 2025-12-22 19:35:57 +05:30
Shivam Mishra c811d698df feat: rebuild pnpm lock 2025-12-22 18:18:38 +05:30
Shivam MishraandGitHub ae9b2daf84 Merge branch 'develop' into feat/ui-lib 2025-12-22 18:17:12 +05:30
Shivam Mishra 73bd05a941 feat: push modal to shadow root in isolated shell mode 2025-12-15 15:39:23 +05:30
Shivam Mishra 9ea746df9a feat: try modal target 2025-12-15 15:22:32 +05:30
Shivam Mishra 44d363f437 feat: always allow gallery 2025-12-15 14:39:45 +05:30
Shivam Mishra 391b139652 feat: use woot modal component directly 2025-12-15 14:37:06 +05:30
Shivam Mishra 55d1e249dc fix: email channel check 2025-12-12 12:45:22 +05:30
Shivam Mishra 852f23f0f5 fix: export 2025-12-11 19:13:17 +05:30
Shivam Mishra 443991078a fix: import 2025-12-11 19:12:10 +05:30
Sivin VargheseandGitHub 5c3ba3ccf2 Merge branch 'develop' into feat/ui-lib 2025-12-11 12:31:46 +05:30
Shivam Mishra f161d8bf23 feat: remove define flag 2025-12-09 20:48:43 +05:30
Shivam Mishra aebb93f88c fix: env 2025-12-09 20:47:52 +05:30
Shivam MishraandGitHub 2a824151cb Merge branch 'develop' into feat/ui-lib 2025-12-09 18:09:56 +05:30
Shivam Mishra 219ce15302 chore: remove currentuserid prop 2025-12-09 17:26:20 +05:30
Shivam Mishra 1c363262b6 feat: allow group with next 2025-12-09 17:26:06 +05:30
Shivam Mishra 1dcafe6e47 chore: remove usage file 2025-12-09 17:11:48 +05:30
Shivam Mishra 491569fa4f chore: revert redundant change 2025-12-09 17:09:04 +05:30
Shivam Mishra c421f75bca chore: remove template to simplify diff 2025-12-09 17:07:46 +05:30
Shivam Mishra 52fc0c683d fix: audio recorder toggle 2025-12-09 17:06:52 +05:30
Shivam Mishra 9145b513c8 fix: allow signature toggle 2025-12-09 17:05:52 +05:30
Shivam Mishra 1f7136860b chore: comments on highlight timer check 2025-12-09 17:03:12 +05:30
Shivam Mishra 3758a07a29 feat: disable gallery for isolated shell 2025-12-09 17:01:02 +05:30
Shivam Mishra de9fea0983 chore: remove redundant changes 2025-12-09 16:57:03 +05:30
Shivam Mishra 11917b7bd7 chore: update pnpm lock 2025-12-09 16:55:37 +05:30
Shivam MishraandGitHub d80247b5ee Merge branch 'develop' into feat/ui-lib 2025-12-09 16:54:04 +05:30
Shivam Mishra c8ade357d7 feat: better spacing 2025-12-01 16:21:31 +05:30
Shivam Mishra c548f71c1a feat: override color and message meta 2025-12-01 16:16:12 +05:30
Shivam Mishra faf6b29626 chore: update docs 2025-12-01 15:54:12 +05:30
Shivam Mishra 6535d19945 feat: allow overrides 2025-12-01 15:53:15 +05:30
Shivam Mishra 1844758848 feat: add doc.md 2025-12-01 15:25:06 +05:30
Shivam Mishra 567092b9a8 feat: try runtime themeing 2025-12-01 15:24:54 +05:30
Shivam Mishra 59e5a5feea chore: update pnpm 2025-12-01 14:12:30 +05:30
Shivam MishraandGitHub 52ce0baef7 Merge branch 'develop' into feat/ui-lib 2025-12-01 14:11:07 +05:30
Shivam Mishra 22a0dac4d6 fix: version suffix 2025-10-22 15:11:55 +05:30
Shivam Mishra 2f215a9cff fix: upload styles not getting included 2025-10-22 15:08:55 +05:30
Shivam Mishra 8f8c7fdfe7 fix: css vars targets only root and not shadow dom host 2025-10-22 15:08:44 +05:30
Shivam Mishra f046d6be32 feat: try separate root and host 2025-10-22 14:56:59 +05:30
Shivam MishraandGitHub 3e5928c8e9 Merge branch 'develop' into feat/ui-lib 2025-10-22 14:30:00 +05:30
Shivam Mishra 1fe5a3c8fb feat: move styles to shadow dom 2025-10-16 13:19:16 +05:30
Shivam Mishra 2b933f0166 feat: use react 18 2025-10-16 11:49:42 +05:30
Shivam Mishra d617266258 feat: update components 2025-10-15 16:18:10 +05:30
Shivam Mishra 65a254705a feat: install packages 2025-10-15 16:16:57 +05:30
Shivam Mishra 1ede5bdeec Merge branch 'feat/ui-lib' of github.com:chatwoot/chatwoot into feat/ui-lib 2025-10-15 16:13:15 +05:30
Shivam MishraandGitHub 3a348d594d Merge branch 'develop' into feat/ui-lib 2025-10-15 15:34:45 +05:30
Shivam MishraandGitHub 6b57264fec Merge branch 'develop' into feat/ui-lib 2025-08-14 20:50:39 +05:30
Muhsin KelothandGitHub 5aaa0400af Merge branch 'develop' into feat/ui-lib 2025-08-14 13:57:46 +05:30
Muhsin KelothandGitHub c9bd109dd8 Merge branch 'develop' into feat/ui-lib 2025-06-26 11:07:34 +05:30
Shivam Mishra 8f47496373 feat: update package scripts 2025-06-12 14:26:04 +05:30
Shivam Mishra 0990e33860 fix: enter to send 2025-06-12 14:24:08 +05:30
Shivam Mishra c93c6aed5f fix: realtime events not working 2025-06-12 14:13:39 +05:30
Shivam Mishra 0381aa188a fix: clearMessage error 2025-06-12 13:07:32 +05:30
Shivam Mishra 90e212a81a feat: add copy mode 2025-06-12 13:06:24 +05:30
Shivam Mishra 76e1643ccf feat: expose disable-editor 2025-06-12 09:50:50 +05:30
Shivam Mishra 0b56add844 fix: version 2025-06-11 18:28:04 +05:30
Shivam Mishra e9b99a5811 fix: uploads 2025-06-11 18:09:08 +05:30
Shivam Mishra a910758fd8 fix: colors 2025-06-11 18:08:53 +05:30
Shivam Mishra aa68525b6b fix: name 2025-06-11 17:34:38 +05:30
Shivam Mishra ba1f363a28 fix: gap and text color 2025-06-11 17:34:08 +05:30
Shivam Mishra b8e64722a7 feat: correct name 2025-06-10 20:11:34 +05:30
Shivam Mishra 153d0371ff feat: update readme 2025-06-10 18:54:04 +05:30
Shivam Mishra c66f8600ec feat: disable upload 2025-06-10 18:46:06 +05:30
Shivam Mishra 38b5c61d51 feat: make tailwind work 2025-06-10 18:45:34 +05:30
Shivam Mishra 81f5c9aa12 chore: remove comment 2025-06-10 17:51:20 +05:30
Shivam Mishra 532c5c3324 chore: remove unused file 2025-06-10 17:49:34 +05:30
Shivam Mishra 2dca1eee89 refactor: simplify component loading 2025-06-10 17:49:23 +05:30
Shivam Mishra 851f622fe1 fix: exports 2025-06-10 17:35:45 +05:30
Shivam Mishra 1503ddeda4 feat: remove unused files 2025-06-10 17:32:18 +05:30
Shivam Mishra c645ba3922 feat: add store from CE options 2025-06-10 17:31:52 +05:30
Shivam Mishra 5d64079a5e feat: simplify provider and vue wrapper 2025-06-10 17:31:42 +05:30
Shivam Mishra 82e296b152 feat: add dompurify and fix i18n ijection 2025-06-10 16:31:55 +05:30
Shivam Mishra 016a8541c1 feat: use i18 and other ce options 2025-06-10 16:29:44 +05:30
Shivam Mishra d289daf6ca refactor: component cleanup 2025-06-10 16:29:33 +05:30
Shivam Mishra c8cc157683 refactor: provider 2025-06-10 16:07:35 +05:30
Shivam Mishra 3fd506c27f feat: update publish script 2025-06-10 14:50:59 +05:30
Shivam Mishra d8a1fdbce5 feat: setup vue component 2025-06-10 14:48:37 +05:30
Shivam Mishra 6840108f30 feat: add publishing pipeline 2025-06-10 13:39:57 +05:30
Shivam Mishra d7288835fa feat: vue component for PoC 2025-06-10 13:00:33 +05:30
Shivam Mishra 31e1d59d58 feat: kickstart basic react component 2025-06-10 12:40:52 +05:30
Shivam Mishra 4f55a66fba Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib 2025-06-10 10:17:19 +05:30
Shivam Mishra 312c8a1a74 fix: upload toggle 2025-06-04 16:43:57 +05:30
Shivam Mishra 79f7afaeec feat: allow disabling editor 2025-06-04 16:43:08 +05:30
Shivam Mishra 4e7ca49297 fix: menubar not empty 2025-06-04 16:40:47 +05:30
Shivam Mishra db51d2d011 fix: options 2025-06-04 16:34:58 +05:30
Shivam Mishra 6f86944758 feat: allow option to disable upload 2025-06-04 16:34:47 +05:30
Shivam Mishra d74798fcd5 feat: disable editor menu for ios 2025-06-04 16:34:26 +05:30
Shivam Mishra 394419ce95 feat: allow new confg 2025-06-04 16:09:42 +05:30
Shivam MishraandGitHub 4fe9b88642 Merge branch 'develop' into feat/ui-lib 2025-06-04 15:51:21 +05:30
Shivam Mishra 0f8037b6fc fix: ios virtual keyboard issue on editor focus 2025-05-21 19:59:26 +05:30
Shivam Mishra 38938fe776 feat: hide name in mobile 2025-05-20 16:39:37 +05:30
Shivam Mishra 02ca4136ad fix: message attachments 2025-05-20 16:28:08 +05:30
Shivam Mishra da4049d18a feat: register UI kit and enable gallery 2025-05-20 16:07:33 +05:30
Shivam Mishra 088608abcf feat: disable elastic scrolling 2025-05-19 19:45:02 +05:30
Shivam Mishra afb3c0109b fix: list item styling 2025-05-19 19:12:08 +05:30
Shivam Mishra c54973eeb2 feat: use enter for next line in isolated shell mode 2025-05-19 19:11:07 +05:30
Shivam Mishra cb2785096e feat: ensure inbox was always there 2025-05-19 16:27:21 +05:30
Shivam Mishra 27f35a7dd8 style: if block 2025-05-19 16:11:47 +05:30
Shivam Mishra 59790e38da fix: editor loads with private note color 2025-05-19 16:10:51 +05:30
Shivam Mishra 461f1b3bf0 chore: attempt to fix CW-4353 2025-05-19 15:39:57 +05:30
Shivam Mishra 4056a144e4 feat: add spinner 2025-05-19 15:37:51 +05:30
Shivam Mishra 31681610f2 feat: disable lightbox in UI shell 2025-05-16 22:01:16 +05:30
Shivam Mishra a8b33023f8 feat: auto init if callback is not present 2025-05-16 16:55:31 +05:30
Shivam Mishra 8c5204d0c9 feat: use computed for conversation ID 2025-05-16 16:55:17 +05:30
Shivam Mishra aa5242fdb0 feat: run on callback 2025-05-16 16:35:18 +05:30
Shivam Mishra b591893486 feat: better init 2025-05-16 16:23:56 +05:30
Shivam Mishra a118bef98e feat: use init fn 2025-05-16 16:18:37 +05:30
Shivam Mishra daa54f6866 feat: disable unused options 2025-05-16 16:12:05 +05:30
Shivam Mishra c38e7af0e7 feat: allow cors for api 2025-05-16 09:48:48 +05:30
Shivam Mishra d6f862a85c feat: new lite reply box 2025-05-15 17:06:32 +05:30
Shivam Mishra 13f742fd68 feat: allow customizing reply box 2025-05-15 17:06:20 +05:30
Shivam Mishra b74e78f698 feat: add spinner 2025-05-15 16:10:27 +05:30
Shivam Mishra 5d8646a3f3 feat: smaller bundle 2025-05-14 19:48:10 +05:30
Shivam Mishra 453eba9a81 feat: conditionally show typing indicator 2025-05-14 16:21:14 +05:30
Shivam Mishra 6beb355f29 feat: add typing indicator 2025-05-14 16:01:18 +05:30
Shivam Mishra 980eca46c6 feat: start typing indicator 2025-05-14 15:53:30 +05:30
Shivam Mishra 0868a9af70 refactor: use vue 2025-05-14 14:05:58 +05:30
Shivam Mishra da6fbe8579 feat: make message sending work 2025-05-13 19:59:14 +05:30
Shivam Mishra 2c814fff99 feat: kickstart message editor\ 2025-05-13 19:52:18 +05:30
Shivam Mishra c1c44512d4 feat: remove test component 2025-05-13 19:30:01 +05:30
Shivam Mishra 1138195846 feat: include ui in tailwind content 2025-05-13 19:24:29 +05:30
Shivam Mishra 84d31b91f4 feat: add space for editor 2025-05-13 19:22:36 +05:30
Shivam Mishra 2df2459674 chore: run lint 2025-05-13 19:03:37 +05:30
Shivam Mishra 81e18aaafa feat: fetch inboxes 2025-05-13 19:00:39 +05:30
Shivam Mishra f07a5215cf feat: allow bypassing indexedb cache 2025-05-13 19:00:30 +05:30
Shivam Mishra 1899a31416 feat: infinite scrolling 2025-05-13 18:50:48 +05:30
Shivam Mishra 60f5145365 fix: test 2025-05-12 19:04:53 +05:30
Shivam Mishra e58736e746 feat: kickstart scroll 2025-05-12 18:55:36 +05:30
Shivam Mishra a9fd459cf4 feat: wait for auth 2025-05-12 17:53:27 +05:30
Shivam Mishra 7235e44a9d feat: ignore audio events for ui mode 2025-05-12 17:53:09 +05:30
Shivam Mishra ddd1fb5396 fix: auth payload 2025-05-12 17:52:59 +05:30
Shivam Mishra a97cf6e4ad feat: allow api based auth 2025-05-12 17:35:06 +05:30
Shivam Mishra 4ecc92ad6d feat: setup wss 2025-05-12 17:13:41 +05:30
Shivam Mishra ff70fbe2e4 Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib 2025-05-09 09:39:24 +05:30
Shivam Mishra 4f27f1605a feat: setup conversation list 2025-05-08 20:55:51 +05:30
Shivam Mishra e00b54ce07 feat: include dompurify 2025-05-08 19:47:35 +05:30
Shivam Mishra df20a59a6c fix: route may not be present 2025-05-08 19:47:28 +05:30
Shivam Mishra bb6f7b9fcd feat: messages 2025-05-08 19:40:10 +05:30
Shivam Mishra e28c6312e4 feat: make i18n work 2025-05-08 10:11:01 +05:30
Shivam Mishra fc119d14d9 feat: styles are working 2025-05-08 09:06:49 +05:30
Shivam Mishra 9243b6367c feat: configure i18n (try atleat) 2025-05-08 08:59:59 +05:30
Shivam Mishra 5af8881a5a feat: tester 2025-05-07 22:36:47 +05:30
Shivam Mishra 70029177da debug: patch process.env 2025-05-07 22:36:38 +05:30
Shivam Mishra 1abb04f592 feat: update ui.js 2025-05-07 22:24:27 +05:30
Shivam Mishra 355b587c2c feat: try 2025-05-07 22:13:17 +05:30
Shivam Mishra b22a903446 feat: import all styles 2025-05-07 22:03:01 +05:30
Shivam Mishra 2d5993dfe3 fix: import name 2025-05-07 19:32:22 +05:30
Shivam Mishra db50f42054 feat: kickstart ui 2025-05-07 19:17:37 +05:30
Shivam Mishra f8afc1509f feat: kickstart ui 2025-05-07 19:12:44 +05:30
1764 changed files with 52752 additions and 9400 deletions
+1
View File
@@ -1,3 +1,4 @@
---
ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
- GHSA-57hq-95w6-v4fc # Devise confirmable race condition — patched locally in User model (remove once on Devise 5+)
+4
View File
@@ -94,6 +94,10 @@ yarn-debug.log*
.vscode
.claude/settings.local.json
.cursor
# react component
dist
.codex/
.claude/
CLAUDE.local.md
+1 -1
View File
@@ -1 +1 @@
4.11.2
4.12.0
@@ -0,0 +1,55 @@
module Api::V1::Accounts::Concerns::WhatsappHealthManagement
extend ActiveSupport::Concern
included do
skip_before_action :check_authorization, only: [:health, :register_webhook]
before_action :check_admin_authorization?, only: [:register_webhook]
before_action :validate_whatsapp_cloud_channel, only: [:health, :register_webhook]
end
def sync_templates
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
def register_webhook
Whatsapp::WebhookSetupService.new(@inbox.channel).register_callback
render json: { message: 'Webhook registered successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[INBOX WEBHOOK] Webhook registration failed: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
end
def whatsapp_channel?
@inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
end
def trigger_template_sync
if @inbox.whatsapp?
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
elsif @inbox.twilio? && @inbox.channel.whatsapp?
Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
end
end
end
@@ -4,8 +4,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :validate_limit, only: [:create]
# we are already handling the authorization in fetch inbox
before_action :check_authorization, except: [:show, :health]
before_action :validate_whatsapp_cloud_channel, only: [:health]
before_action :check_authorization, except: [:show]
include Api::V1::Accounts::Concerns::WhatsappHealthManagement
def index
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
@@ -70,23 +71,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
end
def sync_templates
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private
def fetch_inbox
@@ -98,12 +82,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
end
def create_channel
return unless allowed_channel_types.include?(permitted_params[:channel][:type])
@@ -200,18 +178,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def get_channel_attributes(channel_type)
channel_type.constantize.const_defined?(:EDITABLE_ATTRS) ? channel_type.constantize::EDITABLE_ATTRS.presence : []
end
def whatsapp_channel?
@inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
end
def trigger_template_sync
if @inbox.whatsapp?
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
elsif @inbox.twilio? && @inbox.channel.whatsapp?
Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
end
end
end
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
@@ -126,7 +126,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
return unless @hook&.access_token
begin
linear_client = Linear.new(@hook.access_token)
linear_client = Linear.new(@hook.access_token, refresh_token: @hook.settings&.[]('refresh_token'))
linear_client.revoke_token
rescue StandardError => e
Rails.logger.error "Failed to revoke Linear token: #{e.message}"
@@ -79,7 +79,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params
params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link,
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }, { draft_locales: [] }] }
)
end
@@ -19,7 +19,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
contact = @contact
end
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
@contact_inbox.update(hmac_verified: true) if should_verify_hmac?
identify_contact(contact)
end
+39 -12
View File
@@ -2,6 +2,8 @@ class Linear::CallbacksController < ApplicationController
include Linear::IntegrationHelper
def show
return redirect_to(safe_linear_redirect_uri) if params[:code].blank? || account_id.blank?
@response = oauth_client.auth_code.get_token(
params[:code],
redirect_uri: "#{base_url}/linear/callback"
@@ -10,7 +12,7 @@ class Linear::CallbacksController < ApplicationController
handle_response
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to linear_redirect_uri
redirect_to safe_linear_redirect_uri
end
private
@@ -31,22 +33,19 @@ class Linear::CallbacksController < ApplicationController
end
def handle_response
hook = account.hooks.new(
raise ArgumentError, 'Missing access token in Linear OAuth response' if parsed_body['access_token'].blank?
hook = account.hooks.find_or_initialize_by(app_id: 'linear')
hook.assign_attributes(
access_token: parsed_body['access_token'],
status: 'enabled',
app_id: 'linear',
settings: {
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
scope: parsed_body['scope']
}
settings: merged_integration_settings(hook.settings)
)
# You may wonder why we're not handling the refresh token update, since the token will expire only after 10 years, https://github.com/linear/linear/issues/251
hook.save!
redirect_to linear_redirect_uri
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to linear_redirect_uri
redirect_to safe_linear_redirect_uri
end
def account
@@ -54,19 +53,47 @@ class Linear::CallbacksController < ApplicationController
end
def account_id
return unless params[:state]
return @account_id if instance_variable_defined?(:@account_id)
verify_linear_token(params[:state])
@account_id = params[:state].present? ? verify_linear_token(params[:state]) : nil
end
def linear_redirect_uri
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/linear"
end
def safe_linear_redirect_uri
return base_url if account_id.blank?
linear_redirect_uri
rescue StandardError
base_url
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def integration_settings
{
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
expires_on: expires_on,
scope: parsed_body['scope'],
refresh_token: parsed_body['refresh_token']
}.compact
end
def merged_integration_settings(existing_settings)
existing_settings.to_h.with_indifferent_access.merge(integration_settings)
end
def expires_on
return if parsed_body['expires_in'].blank?
(Time.current.utc + parsed_body['expires_in'].to_i.seconds).to_s
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
@@ -6,6 +6,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
layout 'portal'
def index
@search_query = list_params[:query]
@articles = @portal.articles.published.includes(:category, :author)
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
@@ -73,7 +74,9 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale, :sort, :status, :page, :per_page)
@list_params ||= params.permit(:query, :locale, :sort, :status, :page, :per_page).tap do |permitted|
permitted[:query] = permitted[:query].to_s.strip.presence
end
end
def permitted_params
+11 -1
View File
@@ -77,13 +77,23 @@ class WidgetsController < ActionController::Base
end
def allow_iframe_requests
if @web_widget.allowed_domains.blank?
if @web_widget.allowed_domains.blank? || embedded_from_non_web_origin?
response.headers.delete('X-Frame-Options')
else
domains = @web_widget.allowed_domains.split(',').map(&:strip).join(' ')
response.headers['Content-Security-Policy'] = "frame-ancestors #{domains}"
end
end
# Mobile WebViews (iOS/Android) load content from file:// or null origins,
# which cannot match any domain in frame-ancestors. When the per-inbox flag
# is enabled, skip frame-ancestors for these requests.
def embedded_from_non_web_origin?
return false unless @web_widget.allow_mobile_webview?
origin = request.headers['Origin']
origin.blank? || origin == 'null' || origin&.start_with?('file://')
end
end
WidgetsController.prepend_mod_with('WidgetsController')
+6
View File
@@ -1,4 +1,10 @@
module TimezoneHelper
def timezone_name_from_params(timezone, offset)
return timezone if timezone.present? && ActiveSupport::TimeZone[timezone].present?
timezone_name_from_offset(offset)
end
# ActiveSupport TimeZone is not aware of the current time, so ActiveSupport::Timezone[offset]
# would return the timezone without considering day light savings. To get the correct timezone,
# this method uses zone.now.utc_offset for comparison as referenced in the issues below
@@ -15,6 +15,12 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ACCOUNT_ID__) {
// eslint-disable-next-line no-underscore-dangle
return window.__WOOT_ACCOUNT_ID__;
}
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
@@ -14,7 +14,8 @@ class CacheEnabledApiClient extends ApiClient {
}
get(cache = false) {
if (cache) {
// eslint-disable-next-line no-underscore-dangle
if (cache && !window.__WOOT_ISOLATED_SHELL__) {
return this.getFromCache();
}
+12 -1
View File
@@ -8,7 +8,14 @@ import {
} from '../store/utils/api';
export default {
validityCheck() {
async validityCheck() {
if (this.hasAuthToken()) {
const urlData = endPoints('profileUpdate');
const response = await axios.get(urlData.url);
// to match the response signature of the validityCheck endpoint
return Promise.resolve({ data: { payload: response } });
}
const urlData = endPoints('validityCheck');
return axios.get(urlData.url);
},
@@ -31,6 +38,10 @@ export default {
hasAuthCookie() {
return !!Cookies.get('cw_d_session_info');
},
hasAuthToken() {
// eslint-disable-next-line no-underscore-dangle
return !!window.__WOOT_ACCESS_TOKEN__;
},
getAuthData() {
if (this.hasAuthCookie()) {
const savedAuthInfo = Cookies.get('cw_d_session_info');
@@ -9,6 +9,10 @@ class InboxHealthAPI extends ApiClient {
getHealthStatus(inboxId) {
return axios.get(`${this.url}/${inboxId}/health`);
}
registerWebhook(inboxId) {
return axios.post(`${this.url}/${inboxId}/register_webhook`);
}
}
export default new InboxHealthAPI();
+7 -1
View File
@@ -1,7 +1,13 @@
/* global axios */
import CacheEnabledApiClient from './CacheEnabledApiClient';
import ApiClient from './ApiClient';
class Inboxes extends CacheEnabledApiClient {
// eslint-disable-next-line no-underscore-dangle
const BaseClass = window.__WOOT_ISOLATED_SHELL__
? ApiClient
: CacheEnabledApiClient;
class Inboxes extends BaseClass {
constructor() {
super('inboxes', { accountScoped: true });
}
@@ -1,7 +1,9 @@
// scss-lint:disable PropertySortOrder
// NEXT COLORS START
@layer base {
// NEXT COLORS START
:root {
:root,
:host {
// slate
--slate-1: 252 252 253;
--slate-2: 249 249 251;
@@ -138,6 +140,48 @@
--label-background: 247 247 247;
--label-border: 0, 0, 0, 0.04;
// Bubble-specific colors (override these for custom theming)
--bubble-agent-bg: var(--solid-blue);
--bubble-agent-text: var(--slate-12);
--bubble-user-bg: var(--slate-4);
--bubble-user-text: var(--slate-12);
--bubble-private-bg: var(--solid-amber);
--bubble-private-text: var(--amber-12);
--bubble-bot-bg: var(--solid-iris);
--bubble-bot-text: var(--slate-12);
// Bubble border customization
--bubble-border-width: 0;
--bubble-agent-border: var(--blue-6);
--bubble-user-border: var(--slate-6);
--bubble-private-border: var(--amber-6);
--bubble-bot-border: var(--iris-6);
// Bubble spacing (ratio of 1 = default, 1.5 = 50% larger, 0.5 = 50% smaller)
--bubble-spacing-ratio: 1;
--bubble-radius: calc(0.75rem * var(--bubble-spacing-ratio));
--bubble-radius-sm: calc(0.125rem * var(--bubble-spacing-ratio));
--bubble-radius-md: calc(0.375rem * var(--bubble-spacing-ratio));
--bubble-radius-lg: calc(0.5rem * var(--bubble-spacing-ratio));
--bubble-padding-x: calc(1rem * var(--bubble-spacing-ratio));
--bubble-padding-y: calc(0.75rem * var(--bubble-spacing-ratio));
// Bubble meta text colors (per variant)
--bubble-agent-meta: var(--slate-11);
--bubble-user-meta: var(--slate-11);
--bubble-private-meta: var(--amber-12);
--bubble-bot-meta: var(--slate-11);
// Message status colors (per variant)
--bubble-agent-status: var(--slate-10);
--bubble-agent-status-read: 126 182 255;
--bubble-user-status: var(--slate-10);
--bubble-user-status-read: 126 182 255;
--bubble-private-status: var(--amber-11);
--bubble-private-status-read: 126 182 255;
--bubble-bot-status: var(--slate-10);
--bubble-bot-status-read: 126 182 255;
--alpha-1: 215, 215, 215, 0.22;
--alpha-2: 196, 197, 198, 0.22;
--alpha-3: 255, 255, 255, 0.96;
@@ -147,7 +191,8 @@
--white-alpha: 255, 255, 255, 0.8;
}
.dark {
.dark,
:host(.dark) {
// slate
--slate-1: 17 17 19;
--slate-2: 24 25 27;
@@ -283,6 +328,48 @@
--label-background: 36 38 45;
--label-border: 255, 255, 255, 0.03;
// Bubble-specific colors (override these for custom theming)
--bubble-agent-bg: var(--solid-blue);
--bubble-agent-text: var(--slate-12);
--bubble-user-bg: var(--slate-4);
--bubble-user-text: var(--slate-12);
--bubble-private-bg: var(--solid-amber);
--bubble-private-text: var(--amber-12);
--bubble-bot-bg: var(--solid-iris);
--bubble-bot-text: var(--slate-12);
// Bubble border customization
--bubble-border-width: 0;
--bubble-agent-border: var(--blue-6);
--bubble-user-border: var(--slate-6);
--bubble-private-border: var(--amber-6);
--bubble-bot-border: var(--iris-6);
// Bubble spacing (inherited from light mode, can be overridden)
--bubble-spacing-ratio: 1;
--bubble-radius: calc(0.75rem * var(--bubble-spacing-ratio));
--bubble-radius-sm: calc(0.125rem * var(--bubble-spacing-ratio));
--bubble-radius-md: calc(0.375rem * var(--bubble-spacing-ratio));
--bubble-radius-lg: calc(0.5rem * var(--bubble-spacing-ratio));
--bubble-padding-x: calc(1rem * var(--bubble-spacing-ratio));
--bubble-padding-y: calc(0.75rem * var(--bubble-spacing-ratio));
// Bubble meta text colors (per variant)
--bubble-agent-meta: var(--slate-11);
--bubble-user-meta: var(--slate-11);
--bubble-private-meta: var(--amber-12);
--bubble-bot-meta: var(--slate-11);
// Message status colors (per variant)
--bubble-agent-status: var(--slate-10);
--bubble-agent-status-read: 126 182 255;
--bubble-user-status: var(--slate-10);
--bubble-user-status-read: 126 182 255;
--bubble-private-status: var(--amber-11);
--bubble-private-status-read: 126 182 255;
--bubble-bot-status: var(--slate-10);
--bubble-bot-status-read: 126 182 255;
--alpha-1: 35, 36, 42, 0.8;
--alpha-2: 147, 153, 176, 0.12;
--alpha-3: 33, 34, 38, 0.95;
@@ -293,4 +380,3 @@
--white-alpha: 255, 255, 255, 0.1;
}
}
// NEXT COLORS END
@@ -26,6 +26,7 @@ const onPortalCreate = ({ slug: portalSlug, locale }) => {
<EmptyStateLayout
:title="$t('HELP_CENTER.TITLE')"
:subtitle="$t('HELP_CENTER.NEW_PAGE.DESCRIPTION')"
class="bg-n-surface-1"
>
<template #empty-state-item>
<div class="grid grid-cols-2 gap-4 p-px">
@@ -1,8 +1,22 @@
<script setup>
import LocaleCard from './LocaleCard.vue';
const locales = [
{ name: 'English', isDefault: true, articleCount: 29, categoryCount: 5 },
{ name: 'Spanish', isDefault: false, articleCount: 29, categoryCount: 5 },
{
name: 'English',
code: 'en',
isDefault: true,
isDraft: false,
articleCount: 29,
categoryCount: 5,
},
{
name: 'Spanish',
code: 'es',
isDefault: false,
isDraft: true,
articleCount: 29,
categoryCount: 5,
},
];
</script>
@@ -19,6 +33,8 @@ const locales = [
<LocaleCard
:locale="locale.name"
:is-default="locale.isDefault"
:is-draft="locale.isDraft"
:locale-code="locale.code"
:article-count="locale.articleCount"
:category-count="locale.categoryCount"
/>
@@ -2,7 +2,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import { LOCALE_MENU_ITEMS } from 'dashboard/helper/portalHelper';
import { buildLocaleMenuItems } from 'dashboard/helper/portalHelper';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
@@ -17,6 +17,10 @@ const props = defineProps({
type: Boolean,
required: true,
},
isDraft: {
type: Boolean,
required: true,
},
localeCode: {
type: String,
required: true,
@@ -37,11 +41,28 @@ const { t } = useI18n();
const [showDropdownMenu, toggleDropdown] = useToggle();
const localeLabel = computed(() => `${props.locale} (${props.localeCode})`);
const localeMenuLabels = computed(() => ({
'change-default': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MAKE_DEFAULT'
),
'move-to-draft': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MOVE_TO_DRAFT'
),
'publish-locale': t(
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.PUBLISH_LOCALE'
),
delete: t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE'),
}));
const localeMenuItems = computed(() =>
LOCALE_MENU_ITEMS.map(item => ({
buildLocaleMenuItems({
isDefault: props.isDefault,
isDraft: props.isDraft,
}).map(item => ({
...item,
label: t(item.label),
disabled: props.isDefault,
label: localeMenuLabels.value[item.action],
}))
);
@@ -56,7 +77,7 @@ const handleAction = ({ action, value }) => {
<div class="flex justify-between gap-2">
<div class="flex items-center justify-start gap-2">
<span class="text-sm font-medium text-n-slate-12 line-clamp-1">
{{ locale }} ({{ localeCode }})
{{ localeLabel }}
</span>
<span
v-if="isDefault"
@@ -64,6 +85,12 @@ const handleAction = ({ action, value }) => {
>
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DEFAULT') }}
</span>
<span
v-else-if="isDraft"
class="bg-n-alpha-2 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-n-slate-11 px-2 py-0.5"
>
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DRAFT') }}
</span>
</div>
<div class="flex items-center justify-end gap-4">
<div class="flex items-center gap-4">
@@ -86,6 +113,7 @@ const handleAction = ({ action, value }) => {
</span>
</div>
<div
v-if="localeMenuItems.length"
v-on-clickaway="() => toggleDropdown(false)"
class="relative group"
>
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import { useAlert, useTrack } from 'dashboard/composables';
@@ -24,12 +24,20 @@ const dialogRef = ref(null);
const isUpdating = ref(false);
const selectedLocale = ref('');
const localeStatus = ref('published');
const addedLocales = computed(() => {
const { allowed_locales: allowedLocales = [] } = props.portal?.config || {};
return allowedLocales.map(locale => locale.code);
});
const draftedLocales = computed(() => {
const { allowed_locales: allowedLocales = [] } = props.portal?.config || {};
return allowedLocales
.filter(locale => locale.draft)
.map(locale => locale.code);
});
const locales = computed(() => {
return Object.keys(allLocales)
.map(key => {
@@ -41,17 +49,44 @@ const locales = computed(() => {
.filter(locale => !addedLocales.value.includes(locale.value));
});
const statusOptions = computed(() => [
{
value: 'published',
label: t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.OPTIONS.LIVE'),
},
{
value: 'draft',
label: t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.OPTIONS.DRAFT'),
},
]);
const resetForm = () => {
selectedLocale.value = '';
localeStatus.value = 'published';
};
watch(localeStatus, value => {
if (!value) {
localeStatus.value = 'published';
}
});
const onCreate = async () => {
if (!selectedLocale.value) return;
isUpdating.value = true;
const updatedLocales = [...addedLocales.value, selectedLocale.value];
const updatedDraftLocales =
localeStatus.value === 'draft'
? [...new Set([...draftedLocales.value, selectedLocale.value])]
: draftedLocales.value;
try {
await store.dispatch('portals/update', {
portalSlug: props.portal?.slug,
config: {
allowed_locales: updatedLocales,
draft_locales: updatedDraftLocales,
default_locale: props.portal?.meta?.default_locale,
},
});
@@ -62,7 +97,7 @@ const onCreate = async () => {
from: route.name,
});
selectedLocale.value = '';
resetForm();
dialogRef.value?.close();
useAlert(
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.API.SUCCESS_MESSAGE')
@@ -87,6 +122,7 @@ defineExpose({ dialogRef });
type="edit"
:title="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.TITLE')"
:description="t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.DESCRIPTION')"
@close="resetForm"
@confirm="onCreate"
>
<div class="flex flex-col gap-6">
@@ -98,6 +134,16 @@ defineExpose({ dialogRef });
"
class="[&>div>button:not(.focused)]:!outline-n-slate-5 [&>div>button:not(.focused)]:dark:!outline-n-slate-5"
/>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-n-slate-12">
{{ t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.STATUS.LABEL') }}
</span>
<ComboBox
v-model="localeStatus"
:options="statusOptions"
class="[&>div>button:not(.focused)]:!outline-n-slate-5 [&>div>button:not(.focused)]:dark:!outline-n-slate-5"
/>
</div>
</div>
</Dialog>
</template>
@@ -29,6 +29,7 @@ const isLocaleDefault = code => {
const updatePortalLocales = async ({
newAllowedLocales,
newDraftLocales,
defaultLocale,
messageKey,
}) => {
@@ -39,6 +40,7 @@ const updatePortalLocales = async ({
config: {
default_locale: defaultLocale,
allowed_locales: newAllowedLocales,
draft_locales: newDraftLocales,
},
});
@@ -53,8 +55,12 @@ const updatePortalLocales = async ({
const changeDefaultLocale = ({ localeCode }) => {
const newAllowedLocales = props.locales.map(locale => locale.code);
const newDraftLocales = props.locales
.filter(locale => locale.isDraft)
.map(locale => locale.code);
updatePortalLocales({
newAllowedLocales,
newDraftLocales,
defaultLocale: localeCode,
messageKey: 'CHANGE_DEFAULT_LOCALE',
});
@@ -81,11 +87,15 @@ const deletePortalLocale = async ({ localeCode }) => {
const updatedLocales = props.locales
.filter(locale => locale.code !== localeCode)
.map(locale => locale.code);
const updatedDraftLocales = props.locales
.filter(locale => locale.code !== localeCode && locale.isDraft)
.map(locale => locale.code);
const defaultLocale = props.portal.meta.default_locale;
await updatePortalLocales({
newAllowedLocales: updatedLocales,
newDraftLocales: updatedDraftLocales,
defaultLocale,
messageKey: 'DELETE_LOCALE',
});
@@ -98,9 +108,46 @@ const deletePortalLocale = async ({ localeCode }) => {
});
};
const updateDraftLocales = async ({ localeCode, shouldDraft, messageKey }) => {
const newAllowedLocales = props.locales.map(locale => locale.code);
const currentDraftLocales = props.locales
.filter(locale => locale.isDraft)
.map(locale => locale.code);
const newDraftLocales = shouldDraft
? [...new Set([...currentDraftLocales, localeCode])]
: currentDraftLocales.filter(locale => locale !== localeCode);
await updatePortalLocales({
newAllowedLocales,
newDraftLocales,
defaultLocale: props.portal.meta.default_locale,
messageKey,
});
};
const moveLocaleToDraft = async ({ localeCode }) => {
await updateDraftLocales({
localeCode,
shouldDraft: true,
messageKey: 'DRAFT_LOCALE',
});
};
const publishLocale = async ({ localeCode }) => {
await updateDraftLocales({
localeCode,
shouldDraft: false,
messageKey: 'PUBLISH_LOCALE',
});
};
const handleAction = ({ action }, localeCode) => {
if (action === 'change-default') {
changeDefaultLocale({ localeCode: localeCode });
} else if (action === 'move-to-draft') {
moveLocaleToDraft({ localeCode: localeCode });
} else if (action === 'publish-locale') {
publishLocale({ localeCode: localeCode });
} else if (action === 'delete') {
deletePortalLocale({ localeCode: localeCode });
}
@@ -114,6 +161,7 @@ const handleAction = ({ action }, localeCode) => {
:key="index"
:locale="locale.name"
:is-default="isLocaleDefault(locale.code)"
:is-draft="locale.isDraft"
:locale-code="locale.code"
:article-count="locale.articlesCount || 0"
:category-count="locale.categoriesCount || 0"
@@ -4,37 +4,49 @@ import LocalesPage from './LocalesPage.vue';
const locales = [
{
name: 'English (en-US)',
code: 'en',
isDefault: true,
isDraft: false,
articleCount: 5,
categoryCount: 5,
},
{
name: 'Spanish (es-ES)',
code: 'es',
isDefault: false,
isDraft: true,
articleCount: 20,
categoryCount: 10,
},
{
name: 'English (en-UK)',
code: 'en_GB',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malay (ms-MY)',
code: 'ms',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malayalam (ml-IN)',
code: 'ml',
isDefault: false,
isDraft: false,
articleCount: 10,
categoryCount: 5,
},
{
name: 'Hindi (hi-IN)',
code: 'hi',
isDefault: false,
isDraft: false,
articleCount: 15,
categoryCount: 7,
},
@@ -14,6 +14,10 @@ defineProps({
type: Boolean,
default: false,
},
hideToggle: {
type: Boolean,
default: false,
},
});
const modelValue = defineModel({ type: Boolean, default: false });
@@ -28,7 +32,8 @@ const modelValue = defineModel({ type: Boolean, default: false });
<span class="text-heading-3 text-n-slate-12">
{{ header }}
</span>
<ToggleSwitch v-model="modelValue" />
<div v-if="hideToggle" class="size-2" />
<ToggleSwitch v-else v-model="modelValue" />
</div>
<span v-if="description" class="text-body-main text-n-slate-11">
{{ description }}
@@ -80,6 +80,12 @@ const sendMessage = async () => {
isLoading.value = false;
}
};
const handleEnterKey = event => {
if (event.isComposing) return;
event.preventDefault();
sendMessage();
};
</script>
<template>
@@ -113,7 +119,7 @@ const sendMessage = async () => {
v-model="newMessage"
class="flex-1 bg-transparent border-none focus:outline-none text-sm mb-0 text-n-slate-12 placeholder:text-n-slate-10"
:placeholder="t('CAPTAIN.PLAYGROUND.MESSAGE_PLACEHOLDER')"
@keyup.enter="sendMessage"
@keydown.enter.exact="handleEnterKey"
/>
<NextButton
ghost
@@ -27,6 +27,7 @@ const initialState = {
conversationFaqs: false,
memories: false,
citations: false,
contactAttributes: false,
},
};
@@ -59,6 +60,7 @@ const updateStateFromAssistant = assistant => {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
citations: config.feature_citation || false,
contactAttributes: config.feature_contact_attributes || false,
};
};
@@ -79,6 +81,7 @@ const handleBasicInfoUpdate = async () => {
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
feature_citation: state.features.citations,
feature_contact_attributes: state.features.contactAttributes,
},
};
@@ -138,6 +141,10 @@ watch(
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
<label class="flex items-center gap-2">
<input v-model="state.features.contactAttributes" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONTACT_ATTRIBUTES') }}
</label>
</div>
</div>
@@ -29,6 +29,12 @@ const handleInput = () => {
nextTick(adjustHeight);
};
const handleEnterKey = event => {
if (event.isComposing) return;
event.preventDefault();
sendMessage();
};
onMounted(() => {
nextTick(adjustHeight);
});
@@ -43,7 +49,7 @@ onMounted(() => {
class="w-full reset-base bg-n-alpha-3 ltr:pl-4 ltr:pr-12 rtl:pl-12 rtl:pr-4 py-3 text-sm border border-n-weak rounded-lg focus:outline-0 focus:outline-none focus:ring-2 focus:ring-n-blue-11 focus:border-n-blue-11 resize-none overflow-hidden max-h-[200px] mb-0 text-n-slate-12"
rows="1"
@input="handleInput"
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.exact="handleEnterKey"
/>
<button
class="absolute ltr:right-1 rtl:left-1 top-1/2 -translate-y-1/2 h-9 w-10 flex items-center justify-center text-n-slate-11 hover:text-n-blue-11"
@@ -134,6 +134,11 @@ const props = defineProps({
senderId: { type: Number, default: null },
senderType: { type: String, default: null },
sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties
forceAlignTo: {
type: String,
default: null,
validator: value => value === null || ['left', 'right'].includes(value),
},
});
const emit = defineEmits(['retry']);
@@ -226,12 +231,16 @@ const isBotOrAgentMessage = computed(() => {
* @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation
*/
const orientation = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return ORIENTATION.CENTER;
if (props.forceAlignTo) {
return props.forceAlignTo === 'left' ? ORIENTATION.LEFT : ORIENTATION.RIGHT;
}
if (isBotOrAgentMessage.value) {
return ORIENTATION.RIGHT;
}
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return ORIENTATION.CENTER;
return ORIENTATION.LEFT;
});
@@ -246,6 +255,10 @@ const flexOrientationClass = computed(() => {
});
const gridClass = computed(() => {
if (props.forceAlignTo) {
return 'grid gap-y-2';
}
const map = {
[ORIENTATION.LEFT]: 'grid grid-cols-1fr',
[ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]',
@@ -255,6 +268,14 @@ const gridClass = computed(() => {
});
const gridTemplate = computed(() => {
if (props.forceAlignTo) {
return `
"avatar"
"bubble"
"meta"
`;
}
const map = {
[ORIENTATION.LEFT]: `
"bubble"
@@ -277,11 +298,19 @@ const shouldGroupWithNext = computed(() => {
const shouldShowAvatar = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
if (props.forceAlignTo) return true;
if (orientation.value === ORIENTATION.LEFT) return false;
return true;
});
const shouldShowSenderName = computed(() => {
if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false;
if (!props.forceAlignTo) return false;
return true;
});
const componentToRender = computed(() => {
if (props.isEmailInbox && !props.private) {
const emailInboxTypes = [MESSAGE_TYPES.INCOMING, MESSAGE_TYPES.OUTGOING];
@@ -344,6 +373,8 @@ const componentToRender = computed(() => {
});
const shouldShowContextMenu = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_DISABLE_CONTEXT_MENU__) return false;
return !props.contentAttributes?.isUnsupported;
});
@@ -410,6 +441,12 @@ const shouldRenderMessage = computed(() => {
});
function openContextMenu(e) {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_DISABLE_CONTEXT_MENU__) {
e.preventDefault();
return;
}
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
['a', 'img'].includes(e.target?.tagName.toLowerCase());
@@ -490,7 +527,8 @@ const avatarTooltip = computed(() => {
});
const setupHighlightTimer = () => {
if (Number(route.query.messageId) !== Number(props.id)) {
// This is a defensive check, since in the ISOLATED shell mode, there might be no route.query
if (Number(route?.query?.messageId) !== Number(props.id)) {
return;
}
@@ -548,15 +586,23 @@ provideMessageContext({
<div
v-if="!shouldGroupWithNext && shouldShowAvatar"
v-tooltip.left-end="avatarTooltip"
class="[grid-area:avatar] flex items-end"
class="[grid-area:avatar] flex items-end gap-2"
>
<Avatar v-bind="avatarInfo" :size="24" />
<span
v-if="shouldShowSenderName"
class="text-sm font-semibold text-n-slate-12 pb-0.5"
>
{{ avatarInfo.name }}
</span>
</div>
<div
class="[grid-area:bubble] flex"
:class="{
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
'ltr:ml-8 rtl:mr-8 justify-end':
!props.forceAlignTo && orientation === ORIENTATION.RIGHT,
'ltr:mr-8 rtl:ml-8':
!props.forceAlignTo && orientation === ORIENTATION.LEFT,
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
}"
@contextmenu="openContextMenu($event)"
@@ -1,7 +1,7 @@
<script setup>
import { defineProps, computed, reactive } from 'vue';
import Message from './Message.vue';
import { MESSAGE_TYPES } from './constants.js';
import shouldGroupWithNext from './groupWithNext.js';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { useMapGetter } from 'dashboard/composables/store.js';
import MessageApi from 'dashboard/api/inbox/message.js';
@@ -90,39 +90,6 @@ const fetchReplyMessage = async (messageId, conversationId) => {
}
};
/**
* Determines if a message should be grouped with the next message
* @param {Number} index - Index of the current message
* @param {Array} searchList - Array of messages to check
* @returns {Boolean} - Whether the message should be grouped with next
*/
const shouldGroupWithNext = (index, searchList) => {
if (index === searchList.length - 1) return false;
const current = searchList[index];
const next = searchList[index + 1];
if (next.status === 'failed') return false;
const nextSenderId = next.senderId ?? next.sender?.id;
const currentSenderId = current.senderId ?? current.sender?.id;
const hasSameSender = nextSenderId === currentSenderId;
const nextMessageType = next.messageType;
const currentMessageType = current.messageType;
const areBothTemplates =
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
currentMessageType === MESSAGE_TYPES.TEMPLATE;
if (!hasSameSender || areBothTemplates) return false;
if (currentMessageType !== nextMessageType) return false;
// Check if messages are in the same minute by rounding down to nearest minute
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
};
/**
* Gets the message that was replied to
* @param {Object} parentMessage - The message containing the reply reference
@@ -9,6 +9,21 @@ import { useMessageContext } from './provider.js';
import { MESSAGE_STATUS, MESSAGE_TYPES } from './constants';
const {
status,
isPrivate,
createdAt,
sourceId,
messageType,
contentAttributes,
inboxId,
} = useMessageContext();
// Resolve channel type from the message's own inbox so that the status
// indicator does not depend on `getSelectedChat` being populated. In the
// Agent SDK shell `setActiveChat` runs from a watcher in MessageList and can
// race with the first render of MessageMeta, leaving `currentChat` empty and
// every channel check returning false (the spinner stays forever).
const {
isAFacebookInbox,
isALineChannel,
@@ -21,16 +36,7 @@ const {
isAnEmailChannel,
isAnInstagramChannel,
isATiktokChannel,
} = useInbox();
const {
status,
isPrivate,
createdAt,
sourceId,
messageType,
contentAttributes,
} = useMessageContext();
} = useInbox(inboxId.value);
const readableTime = computed(() =>
messageTimestamp(createdAt.value, 'LLL d, h:mm a')
@@ -67,6 +73,9 @@ const isSent = computed(() => {
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
}
// API inbox messages use real sent/delivered/read status values from the external system.
if (isAPIInbox.value) return status.value === MESSAGE_STATUS.SENT;
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (isALineChannel.value) return true;
@@ -86,8 +95,10 @@ const isDelivered = computed(() => {
) {
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
}
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (isAWebWidgetInbox.value || isAPIInbox.value) {
// API inbox messages use real delivered status from the external system.
if (isAPIInbox.value) return status.value === MESSAGE_STATUS.DELIVERED;
// All messages marked as delivered for the web widget inbox once they are sent.
if (isAWebWidgetInbox.value) {
return status.value === MESSAGE_STATUS.SENT;
}
if (isALineChannel.value) {
@@ -2,7 +2,8 @@
import { computed, ref } from 'vue';
import { useIntervalFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import { MESSAGE_STATUS } from './constants';
import { MESSAGE_STATUS, MESSAGE_VARIANTS } from './constants';
import { useMessageContext } from './provider.js';
import Icon from 'next/icon/Icon.vue';
@@ -15,6 +16,19 @@ const { status } = defineProps({
});
const { t } = useI18n();
const { variant } = useMessageContext();
const variantPrefix = computed(() => {
const prefixMap = {
[MESSAGE_VARIANTS.AGENT]: 'agent',
[MESSAGE_VARIANTS.USER]: 'user',
[MESSAGE_VARIANTS.PRIVATE]: 'private',
[MESSAGE_VARIANTS.BOT]: 'bot',
[MESSAGE_VARIANTS.TEMPLATE]: 'bot',
[MESSAGE_VARIANTS.EMAIL]: 'agent',
};
return prefixMap[variant.value] || 'agent';
});
const progresIconSequence = [
'i-lucide-clock-1',
@@ -54,14 +68,44 @@ const statusIcon = computed(() => {
return statusIconMap[status];
});
const statusColor = computed(() => {
const statusIconMap = {
[MESSAGE_STATUS.SENT]: 'text-n-slate-10',
[MESSAGE_STATUS.DELIVERED]: 'text-n-slate-10',
[MESSAGE_STATUS.READ]: 'text-[#7EB6FF]',
};
const statusColorMap = {
agent: {
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-agent-status))]',
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-agent-status))]',
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-agent-status-read))]',
},
user: {
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-user-status))]',
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-user-status))]',
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-user-status-read))]',
},
private: {
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-private-status))]',
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-private-status))]',
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-private-status-read))]',
},
bot: {
[MESSAGE_STATUS.SENT]: 'text-[rgb(var(--bubble-bot-status))]',
[MESSAGE_STATUS.DELIVERED]: 'text-[rgb(var(--bubble-bot-status))]',
[MESSAGE_STATUS.READ]: 'text-[rgb(var(--bubble-bot-status-read))]',
},
};
return statusIconMap[status];
const statusColor = computed(() => {
const variantMap =
statusColorMap[variantPrefix.value] || statusColorMap.agent;
return variantMap[status];
});
const progressColorMap = {
agent: 'text-[rgb(var(--bubble-agent-status))]',
user: 'text-[rgb(var(--bubble-user-status))]',
private: 'text-[rgb(var(--bubble-private-status))]',
bot: 'text-[rgb(var(--bubble-bot-status))]',
};
const progressColor = computed(() => {
return progressColorMap[variantPrefix.value] || progressColorMap.agent;
});
const tooltipText = computed(() => {
@@ -81,7 +125,7 @@ const tooltipText = computed(() => {
v-if="status === MESSAGE_STATUS.PROGRESS"
v-tooltip.top-start="tooltipText"
:icon="progessIcon"
class="text-n-slate-10"
:class="progressColor"
/>
<Icon
v-else
@@ -0,0 +1,45 @@
<template>
<svg
class="w-8 h-4"
viewBox="0 0 120 30"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<circle cx="15" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="55" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.1s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="95" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.2s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
</svg>
</template>
@@ -3,19 +3,31 @@ import { computed } from 'vue';
import BaseBubble from './Base.vue';
import AudioChip from 'next/message/chips/Audio.vue';
import { useMessageContext } from '../provider.js';
import { MESSAGE_VARIANTS } from '../constants';
const { attachments } = useMessageContext();
const { attachments, variant } = useMessageContext();
const attachment = computed(() => {
return attachments.value[0];
});
const textClass = computed(() => {
const textClassMap = {
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-text))]',
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-text))]',
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-text))]',
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-text))]',
};
return textClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-text))]';
});
</script>
<template>
<BaseBubble class="bg-transparent" data-bubble-name="audio">
<AudioChip
:attachment="attachment"
class="p-2 text-n-slate-12 skip-context-menu"
class="p-2 skip-context-menu"
:class="textClass"
/>
</BaseBubble>
</template>
@@ -20,25 +20,30 @@ const { variant, orientation, inReplyTo, shouldGroupWithNext } =
const { t } = useI18n();
const varaintBaseMap = {
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
[MESSAGE_VARIANTS.AGENT]:
'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-agent-border))]',
[MESSAGE_VARIANTS.PRIVATE]:
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
'bg-[rgb(var(--bubble-private-bg))] text-[rgb(var(--bubble-private-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-private-border))] [&_.prosemirror-mention-node]:font-semibold',
[MESSAGE_VARIANTS.USER]:
'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-user-border))]',
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
[MESSAGE_VARIANTS.ERROR]: 'bg-n-ruby-4 text-n-ruby-12',
[MESSAGE_VARIANTS.BOT]:
'bg-[rgb(var(--bubble-bot-bg))] text-[rgb(var(--bubble-bot-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-bot-border))]',
[MESSAGE_VARIANTS.TEMPLATE]:
'bg-[rgb(var(--bubble-bot-bg))] text-[rgb(var(--bubble-bot-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-bot-border))]',
[MESSAGE_VARIANTS.ERROR]:
'bg-[rgb(var(--bubble-error-bg))] text-[rgb(var(--bubble-error-text))] border-[length:var(--bubble-border-width)] border-[rgb(var(--bubble-error-border))]',
[MESSAGE_VARIANTS.EMAIL]: 'w-full',
[MESSAGE_VARIANTS.UNSUPPORTED]:
'bg-n-solid-amber/70 border border-dashed border-n-amber-12 text-n-amber-12',
'bg-[rgb(var(--bubble-unsupported-bg))]/70 border border-dashed border-[rgb(var(--bubble-unsupported-border))] text-[rgb(var(--bubble-unsupported-text))]',
};
const orientationMap = {
[ORIENTATION.LEFT]:
'left-bubble rounded-xl ltr:rounded-bl-sm rtl:rounded-br-sm',
'left-bubble rounded-[var(--bubble-radius)] ltr:rounded-bl-[var(--bubble-radius-sm)] rtl:rounded-br-[var(--bubble-radius-sm)]',
[ORIENTATION.RIGHT]:
'right-bubble rounded-xl ltr:rounded-br-sm rtl:rounded-bl-sm',
[ORIENTATION.CENTER]: 'rounded-md',
'right-bubble rounded-[var(--bubble-radius)] ltr:rounded-br-[var(--bubble-radius-sm)] rtl:rounded-bl-[var(--bubble-radius-sm)]',
[ORIENTATION.CENTER]: 'rounded-[var(--bubble-radius-md)]',
};
const flexOrientationClass = computed(() => {
@@ -57,7 +62,7 @@ const messageClass = computed(() => {
if (variant.value !== MESSAGE_VARIANTS.ACTIVITY) {
classToApply.push(orientationMap[orientation.value]);
} else {
classToApply.push('rounded-lg');
classToApply.push('rounded-[var(--bubble-radius-lg)]');
}
return classToApply;
@@ -76,6 +81,20 @@ const shouldShowMeta = computed(
variant.value !== MESSAGE_VARIANTS.ACTIVITY
);
const metaColorClass = computed(() => {
const metaClassMap = {
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
[MESSAGE_VARIANTS.PRIVATE]:
'text-[rgb(var(--bubble-private-meta))] opacity-50',
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
[MESSAGE_VARIANTS.TEMPLATE]: 'text-[rgb(var(--bubble-bot-meta))]',
[MESSAGE_VARIANTS.EMAIL]: 'text-[rgb(var(--bubble-agent-meta))]',
[MESSAGE_VARIANTS.ERROR]: 'text-[rgb(var(--bubble-error-meta))]',
};
return metaClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
});
const replyToPreview = computed(() => {
if (!inReplyTo) return '';
@@ -119,9 +138,7 @@ const replyToPreview = computed(() => {
:class="[
flexOrientationClass,
variant === MESSAGE_VARIANTS.EMAIL ? 'px-3 pb-3' : '',
variant === MESSAGE_VARIANTS.PRIVATE
? 'text-n-amber-12/50'
: 'text-n-slate-11',
metaColorClass,
]"
class="mt-2"
/>
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useMessageContext } from '../provider.js';
import { MESSAGE_VARIANTS } from '../constants';
defineProps({
icon: { type: [String, Object], required: true },
@@ -20,16 +21,39 @@ defineProps({
},
});
const { sender } = useMessageContext();
const { sender, variant } = useMessageContext();
const { t } = useI18n();
const senderName = computed(() => {
return sender?.value?.name || '';
});
const primaryTextClass = computed(() => {
const classMap = {
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-text))]',
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-text))]',
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-text))]',
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-text))]',
};
return classMap[variant.value] || 'text-[rgb(var(--bubble-agent-text))]';
});
const secondaryTextClass = computed(() => {
const classMap = {
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-meta))]',
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
};
return classMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
});
</script>
<template>
<BaseBubble class="overflow-hidden p-3" data-bubble-name="attachment">
<BaseBubble
class="overflow-hidden p-[var(--bubble-padding-y)]"
data-bubble-name="attachment"
>
<div class="grid gap-4 min-w-64">
<div class="grid gap-3">
<div
@@ -41,7 +65,11 @@ const senderName = computed(() => {
</slot>
</div>
<div class="space-y-1 overflow-hidden">
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
<div
v-if="senderName"
class="text-sm truncate"
:class="primaryTextClass"
>
{{
t(senderTranslationKey, {
sender: senderName,
@@ -49,10 +77,18 @@ const senderName = computed(() => {
}}
</div>
<slot>
<div v-if="title" class="truncate text-sm text-n-slate-12">
<div
v-if="title"
class="truncate text-sm"
:class="primaryTextClass"
>
{{ title }}
</div>
<div v-if="content" class="truncate text-sm text-n-slate-11">
<div
v-if="content"
class="truncate text-sm"
:class="secondaryTextClass"
>
{{ content }}
</div>
</slot>
@@ -40,7 +40,10 @@ const starRatingValue = computed(() => {
</script>
<template>
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
<BaseBubble
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
data-bubble-name="csat"
>
<h4>{{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
<dl v-if="isRatingSubmitted" class="mt-4">
<dt class="text-n-slate-11 italic">
@@ -1,14 +1,32 @@
<script setup>
import { computed } from 'vue';
import { MESSAGE_STATUS } from '../../constants';
import { MESSAGE_STATUS, MESSAGE_TYPES } from '../../constants';
import { useMessageContext } from '../../provider.js';
const { contentAttributes, status, sender } = useMessageContext();
const { contentAttributes, status, sender, messageType } = useMessageContext();
const hasError = computed(() => {
return status.value === MESSAGE_STATUS.FAILED;
});
const isIncoming = computed(() => {
return messageType.value === MESSAGE_TYPES.INCOMING;
});
const secondaryTextClass = computed(() => {
if (hasError.value) return 'text-[rgb(var(--bubble-error-meta))]';
return isIncoming.value
? 'text-[rgb(var(--bubble-user-meta))]'
: 'text-[rgb(var(--bubble-agent-meta))]';
});
const primaryTextClass = computed(() => {
if (hasError.value) return 'text-[rgb(var(--bubble-error-text))]';
return isIncoming.value
? 'text-[rgb(var(--bubble-user-text))]'
: 'text-[rgb(var(--bubble-agent-text))]';
});
const fromEmail = computed(() => {
return contentAttributes.value?.email?.from ?? [];
});
@@ -68,13 +86,10 @@ const showMeta = computed(() => {
<section
v-show="showMeta"
class="space-y-1 rtl:pl-9 ltr:pr-9 text-sm break-words"
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
:class="secondaryTextClass"
>
<template v-if="showMeta">
<div
v-if="fromEmail[0]"
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
>
<div v-if="fromEmail[0]" :class="primaryTextClass">
<template v-if="senderName">
<span>
{{ senderName }}
@@ -104,21 +104,26 @@ const handleSeeOriginal = () => {
<template>
<BaseBubble
class="w-full"
class="w-full border-[length:var(--bubble-border-width)]"
:class="{
'bg-n-slate-4': isIncoming,
'bg-n-solid-blue': isOutgoing,
'bg-[rgb(var(--bubble-user-bg))] text-[rgb(var(--bubble-user-text))] border-[rgb(var(--bubble-user-border))]':
isIncoming,
'bg-[rgb(var(--bubble-agent-bg))] text-[rgb(var(--bubble-agent-text))] border-[rgb(var(--bubble-agent-border))]':
isOutgoing,
}"
data-bubble-name="email"
>
<EmailMeta
class="p-3"
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
:class="{
'border-b border-n-strong': isIncoming,
'border-b border-n-slate-8/20': isOutgoing,
'border-b border-[rgb(var(--bubble-user-border))]': isIncoming,
'border-b border-[rgb(var(--bubble-agent-border))]': isOutgoing,
}"
/>
<section ref="contentContainer" class="p-3">
<section
ref="contentContainer"
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
>
<div
:class="{
'max-h-[400px] overflow-hidden relative': !isExpanded && isExpandable,
@@ -129,14 +134,18 @@ const handleSeeOriginal = () => {
v-if="isExpandable && !isExpanded"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end"
:class="{
'bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent':
'bg-gradient-to-t from-[rgb(var(--bubble-user-bg))] via-[rgb(var(--bubble-user-bg))] via-20% to-transparent':
isIncoming,
'bg-gradient-to-t from-n-solid-blue via-n-solid-blue via-20% to-transparent':
'bg-gradient-to-t from-[rgb(var(--bubble-agent-bg))] via-[rgb(var(--bubble-agent-bg))] via-20% to-transparent':
isOutgoing,
}"
>
<button
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
class="py-2 px-8 mx-auto text-center flex items-center gap-2"
:class="{
'text-[rgb(var(--bubble-user-text))]': isIncoming,
'text-[rgb(var(--bubble-agent-text))]': isOutgoing,
}"
@click="isExpanded = true"
>
<Icon icon="i-lucide-maximize-2" />
@@ -145,7 +154,7 @@ const handleSeeOriginal = () => {
</div>
<FormattedContent
v-if="isOutgoing && content && !hasEmailContent"
class="text-n-slate-12"
class="text-[rgb(var(--bubble-agent-text))]"
:content="messageContent"
/>
<template v-else>
@@ -176,7 +185,11 @@ const handleSeeOriginal = () => {
</template>
<button
v-if="hasQuotedMessage"
class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
class="px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2"
:class="{
'text-[rgb(var(--bubble-user-meta))]': isIncoming,
'text-[rgb(var(--bubble-agent-meta))]': isOutgoing,
}"
@click="showQuotedMessage = !showQuotedMessage"
>
<template v-if="showQuotedMessage">
@@ -48,7 +48,10 @@ const formValues = computed(() => {
</script>
<template>
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
<BaseBubble
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
data-bubble-name="form"
>
<span v-dompurify-html="content" :title="content" />
<dl v-if="formValues.length" class="mt-4">
<template v-for="item in formValues" :key="item.title">
@@ -6,6 +6,7 @@ import { useLoadWithRetry } from 'dashboard/composables/loadWithRetry';
import BaseBubble from './Base.vue';
import Button from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import { downloadFile } from '@chatwoot/utils';
@@ -22,8 +23,8 @@ const attachment = computed(() => {
const { isLoaded, hasError, loadWithRetry } = useLoadWithRetry();
const showGallery = ref(false);
const isDownloading = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
onMounted(() => {
if (attachment.value?.dataUrl) {
@@ -50,13 +51,16 @@ const handleImageError = () => {
<template>
<BaseBubble
class="overflow-hidden p-3"
class="overflow-hidden p-[var(--bubble-padding-y)]"
data-bubble-name="image"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg">
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
<p class="mb-0 text-n-slate-11">
<div
v-if="hasError"
class="flex items-center gap-1 text-center rounded-lg text-[rgb(var(--bubble-error-meta))]"
>
<Icon icon="i-lucide-circle-off" />
<p class="mb-0">
{{ $t('COMPONENTS.MEDIA.IMAGE_UNAVAILABLE') }}
</p>
</div>
@@ -86,7 +90,7 @@ const handleImageError = () => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -44,7 +44,10 @@ const onVideoLoadError = () => {
</script>
<template>
<BaseBubble class="p-3 overflow-hidden" data-bubble-name="ig-story">
<BaseBubble
class="p-[var(--bubble-padding-y)] overflow-hidden"
data-bubble-name="ig-story"
>
<p v-if="isStoryReply" class="mb-1 text-xs text-n-slate-11">
{{ t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_REPLY') }}
</p>
@@ -64,10 +67,10 @@ const onVideoLoadError = () => {
/>
<div
v-else
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1"
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1 text-[rgb(var(--bubble-error-meta))]"
>
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
<p class="mb-0 text-n-slate-11">
<Icon icon="i-lucide-circle-off" />
<p class="mb-0">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
@@ -4,11 +4,11 @@ import BaseBubble from 'next/message/bubbles/Base.vue';
import FormattedContent from './FormattedContent.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
import { MESSAGE_TYPES } from '../../constants';
import { MESSAGE_TYPES, MESSAGE_VARIANTS } from '../../constants';
import { useMessageContext } from '../../provider.js';
import { useTranslations } from 'dashboard/composables/useTranslations';
const { content, attachments, contentAttributes, messageType } =
const { content, attachments, contentAttributes, messageType, variant } =
useMessageContext();
const { hasTranslations, translationContent } =
@@ -36,15 +36,29 @@ const isEmpty = computed(() => {
return !content.value && !attachments.value?.length;
});
const emptyTextClass = computed(() => {
const metaClassMap = {
[MESSAGE_VARIANTS.AGENT]: 'text-[rgb(var(--bubble-agent-meta))]',
[MESSAGE_VARIANTS.USER]: 'text-[rgb(var(--bubble-user-meta))]',
[MESSAGE_VARIANTS.PRIVATE]: 'text-[rgb(var(--bubble-private-meta))]',
[MESSAGE_VARIANTS.BOT]: 'text-[rgb(var(--bubble-bot-meta))]',
[MESSAGE_VARIANTS.TEMPLATE]: 'text-[rgb(var(--bubble-bot-meta))]',
};
return metaClassMap[variant.value] || 'text-[rgb(var(--bubble-agent-meta))]';
});
const handleSeeOriginal = () => {
renderOriginal.value = !renderOriginal.value;
};
</script>
<template>
<BaseBubble class="px-4 py-3" data-bubble-name="text">
<BaseBubble
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)]"
data-bubble-name="text"
>
<div class="gap-3 flex flex-col">
<span v-if="isEmpty" class="text-n-slate-11">
<span v-if="isEmpty" :class="emptyTextClass">
{{ $t('CONVERSATION.NO_CONTENT') }}
</span>
<FormattedContent v-if="renderContent" :content="renderContent" />
@@ -21,7 +21,10 @@ const unsupportedMessageKey = computed(() => {
</script>
<template>
<BaseBubble class="px-4 py-3 text-sm" data-bubble-name="unsupported">
<BaseBubble
class="px-[var(--bubble-padding-x)] py-[var(--bubble-padding-y)] text-sm"
data-bubble-name="unsupported"
>
{{ $t(unsupportedMessageKey) }}
</BaseBubble>
</template>
@@ -2,6 +2,7 @@
import { ref, computed } from 'vue';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -9,7 +10,7 @@ import { ATTACHMENT_TYPES } from '../constants';
const emit = defineEmits(['error']);
const hasError = ref(false);
const showGallery = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
const handleError = () => {
@@ -28,9 +29,9 @@ const isReel = computed(() => {
<template>
<BaseBubble
class="overflow-hidden p-3"
class="overflow-hidden p-[var(--bubble-padding-y)]"
data-bubble-name="video"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<div class="relative group rounded-lg overflow-hidden">
<div
@@ -53,7 +54,7 @@ const isReel = computed(() => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,6 +1,7 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
@@ -13,7 +14,7 @@ defineProps({
},
});
const hasError = ref(false);
const showGallery = ref(false);
const { showGallery, toggleGallery, isGalleryAllowed } = useGallery();
const { filteredCurrentChatAttachments } = useMessageContext();
@@ -25,7 +26,7 @@ const handleError = () => {
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<div
v-if="hasError"
@@ -42,7 +43,7 @@ const handleError = () => {
/>
</div>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,6 +1,6 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -12,7 +12,7 @@ defineProps({
},
});
const showGallery = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const { filteredCurrentChatAttachments } = useMessageContext();
</script>
@@ -20,7 +20,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<video
:src="attachment.dataUrl"
@@ -42,7 +42,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
</div>
</div>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -0,0 +1,33 @@
import { MESSAGE_TYPES } from './constants';
/**
* Determines if a message should be grouped with the next message
* @param {Number} index - Index of the current message
* @param {Array} searchList - Array of messages to check
* @returns {Boolean} - Whether the message should be grouped with next
*/
export default (index, searchList) => {
if (index === searchList.length - 1) return false;
const current = searchList[index];
const next = searchList[index + 1];
if (next.status === 'failed') return false;
const nextSenderId = next.senderId ?? next.sender?.id;
const currentSenderId = current.senderId ?? current.sender?.id;
const hasSameSender = nextSenderId === currentSenderId;
const nextMessageType = next.messageType;
const currentMessageType = current.messageType;
const areBothTemplates =
nextMessageType === MESSAGE_TYPES.TEMPLATE &&
currentMessageType === MESSAGE_TYPES.TEMPLATE;
if (!hasSameSender || areBothTemplates) return false;
if (currentMessageType !== nextMessageType) return false;
// Check if messages are in the same minute by rounding down to nearest minute
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
};
@@ -0,0 +1,20 @@
import { computed } from 'vue';
import { useToggle } from '@vueuse/core';
export function useGallery() {
const [showGallery] = useToggle(false);
const isGalleryAllowed = computed(() => {
return true;
});
const toggleGallery = value => {
showGallery.value = value;
};
return {
showGallery,
isGalleryAllowed,
toggleGallery,
};
}
+49 -27
View File
@@ -15,6 +15,7 @@ const { modalType, closeOnBackdropClick, onClose } = defineProps({
const emit = defineEmits(['close']);
const show = defineModel('show', { type: Boolean, default: false });
const teleportTarget = ref(null);
const modalClassName = computed(() => {
const modalClassNameMap = {
@@ -25,6 +26,24 @@ const modalClassName = computed(() => {
return `modal-mask skip-context-menu ${modalClassNameMap[modalType] || ''}`;
});
const resolveTeleportTarget = () => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) {
// Look for the specific chatwoot-message-list custom element
const messageListElement = document.querySelector('chatwoot-message-list');
if (messageListElement?.shadowRoot) {
const modalRoot =
messageListElement.shadowRoot.querySelector('#cw-modal-root');
if (modalRoot) {
teleportTarget.value = modalRoot;
return;
}
}
}
teleportTarget.value = document.body;
};
// [TODO] Revisit this logic to use outside click directive
const mousedDownOnBackdrop = ref(false);
@@ -58,6 +77,7 @@ useEventListener(document.body, 'mouseup', onMouseUp);
useEventListener(document, 'keydown', onKeydown);
onMounted(() => {
resolveTeleportTarget();
if (import.meta.env.DEV && onClose && typeof onClose === 'function') {
// eslint-disable-next-line no-console
console.warn(
@@ -68,36 +88,38 @@ onMounted(() => {
</script>
<template>
<transition name="modal-fade">
<div
v-if="show"
:class="modalClassName"
transition="modal"
@mousedown="handleMouseDown"
>
<Teleport v-if="teleportTarget" :to="teleportTarget">
<transition name="modal-fade">
<div
class="relative max-h-full overflow-auto bg-n-alpha-3 shadow-md modal-container rtl:text-right skip-context-menu"
:class="{
'rounded-xl w-[37.5rem]': !fullWidth,
'items-center rounded-none flex h-full justify-center w-full':
fullWidth,
[size]: true,
}"
@mouse.stop
@mousedown="event => event.stopPropagation()"
v-if="show"
:class="modalClassName"
transition="modal"
@mousedown="handleMouseDown"
>
<Button
v-if="showCloseButton"
ghost
slate
icon="i-lucide-x"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close"
/>
<slot />
<div
class="relative max-h-full overflow-auto bg-n-alpha-3 shadow-md modal-container rtl:text-right skip-context-menu"
:class="{
'rounded-xl w-[37.5rem]': !fullWidth,
'items-center rounded-none flex h-full justify-center w-full':
fullWidth,
[size]: true,
}"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
<Button
v-if="showCloseButton"
ghost
slate
icon="i-lucide-x"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close"
/>
<slot />
</div>
</div>
</div>
</transition>
</transition>
</Teleport>
</template>
<style lang="scss">
@@ -107,7 +107,8 @@ function onKeydown(view, event) {
emit('keydown');
// Handle Enter key to send message (Shift+Enter for new line)
if (event.key === 'Enter' && !event.shiftKey) {
// Skip if IME composition is active (CJK character confirmation)
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
event.preventDefault();
handleSubmit();
return true; // Prevent ProseMirror's default Enter handling
@@ -89,6 +89,7 @@ const props = defineProps({
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
forceSignature: { type: Boolean, default: false },
conversationId: { type: Number, default: null },
medium: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
@@ -313,6 +314,10 @@ const plugins = computed(() => {
const sendWithSignature = computed(() => {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
if (props.forceSignature && props.allowSignature && !props.isPrivate) {
return true;
}
if (props.allowSignature && !props.isPrivate && props.channelType) {
return fetchSignatureFlagFromUISettings(props.channelType);
}
@@ -564,10 +569,14 @@ function updateImgToolbarOnDelete() {
}
function isEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return isEditorHotKeyEnabled('enter');
}
function isCmdPlusEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return true;
return isEditorHotKeyEnabled('cmd_enter');
}
@@ -811,7 +820,7 @@ watch(sendWithSignature, newValue => {
}
});
onMounted(() => {
onMounted(async () => {
// [VITE] state assignment was done in created before
state = createState(
props.modelValue,
@@ -823,11 +832,16 @@ onMounted(() => {
createEditorView();
editorView.updateState(state);
if (props.focusOnMount) {
// TODO: test this in the main app
if (sendWithSignature.value) {
addSignature();
} else if (props.focusOnMount) {
focusEditorInputField();
}
});
defineExpose({ focusEditorInputField });
// BUS Event to insert text or markdown into the editor at the
// current cursor position.
// Components using this
@@ -915,6 +929,10 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
.ProseMirror-menubar-wrapper {
@apply flex flex-col gap-3;
.ProseMirror-menubar:empty {
display: none;
}
.ProseMirror-menubar {
min-height: 1.25rem !important;
@apply items-center gap-4 flex pb-0 bg-transparent text-n-slate-11 relative ltr:-left-[3px] rtl:-right-[3px];
@@ -2,6 +2,7 @@
import { computed, useTemplateRef } from 'vue';
import { useElementSize } from '@vueuse/core';
import { REPLY_EDITOR_MODES } from './constants';
import Icon from 'next/icon/Icon.vue';
const props = defineProps({
mode: {
@@ -12,6 +13,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
iconOnly: {
type: Boolean,
default: false,
},
isReplyRestricted: {
type: Boolean,
default: false,
@@ -69,22 +74,34 @@ const translateValue = computed(() => {
<template>
<button
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75"
class="flex items-center w-auto p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75"
:disabled="disabled || isReplyRestricted"
:class="{
'h-7': iconOnly,
'h-8': !iconOnly,
'cursor-not-allowed': disabled || isReplyRestricted,
}"
@click="$emit('toggleMode')"
>
<div ref="wootEditorReplyMode" class="flex items-center gap-1 px-2 z-20">
{{ $t('CONVERSATION.REPLYBOX.REPLY') }}
<div
ref="wootEditorReplyMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
<Icon v-if="iconOnly" icon="i-lucide-message-circle" class="size-4" />
<template v-else>{{ $t('CONVERSATION.REPLYBOX.REPLY') }}</template>
</div>
<div ref="wootEditorPrivateMode" class="flex items-center gap-1 px-2 z-20">
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
<div
ref="wootEditorPrivateMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
<Icon v-if="iconOnly" icon="i-lucide-lock-keyhole" class="size-4" />
<template v-else>{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}</template>
</div>
<div
class="absolute shadow-sm rounded-full h-6 w-[var(--chip-width)] ease-in-out translate-x-[var(--translate-x)] rtl:translate-x-[var(--rtl-translate-x)] bg-n-solid-1"
:class="{
'h-5': iconOnly,
'h-6': !iconOnly,
'transition-all duration-300': !disabled && !isReplyRestricted,
}"
:style="{
@@ -114,6 +114,11 @@ export default {
type: String,
default: '',
},
allowSignature: { type: Boolean, default: false },
allowEmoji: { type: Boolean, default: false },
allowVideoCall: { type: Boolean, default: false },
allowFileUpload: { type: Boolean, default: false },
allowAudioRecorder: { type: Boolean, default: false },
showQuotedReplyToggle: {
type: Boolean,
default: false,
@@ -291,7 +296,7 @@ export default {
<div class="flex justify-between p-3" :class="wrapClass">
<div class="left-wrap">
<NextButton
v-if="!isEditorDisabled"
v-if="allowEmoji && !isEditorDisabled"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
icon="i-ph-smiley-sticker"
slate
@@ -300,7 +305,7 @@ export default {
@click="toggleEmojiPicker"
/>
<FileUpload
v-if="showAttachButton"
v-if="showAttachButton && allowFileUpload"
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
@@ -325,7 +330,7 @@ export default {
/>
</FileUpload>
<NextButton
v-if="showAudioRecorderButton"
v-if="allowAudioRecorder && showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'"
slate
@@ -334,7 +339,7 @@ export default {
@click="toggleAudioRecorder"
/>
<NextButton
v-if="showAudioPlayStopButton"
v-if="allowAudioRecorder && showAudioPlayStopButton"
:icon="audioRecorderPlayStopIcon"
slate
faded
@@ -343,7 +348,7 @@ export default {
@click="toggleAudioRecorderPlayPause"
/>
<NextButton
v-if="showMessageSignatureButton"
v-if="showMessageSignatureButton && allowSignature"
v-tooltip.top-end="signatureToggleTooltip"
icon="i-ph-signature"
slate
@@ -381,6 +386,7 @@ export default {
/>
<VideoCallButton
v-if="
allowVideoCall &&
(isAWebWidgetInbox || isAPIInbox) &&
!isOnPrivateNote &&
!isEditorDisabled
@@ -25,6 +25,10 @@ export default {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
disablePopout: {
type: Boolean,
default: false,
},
isReplyRestricted: {
type: Boolean,
default: false,
@@ -99,6 +99,7 @@ export default {
} = useUISettings();
const replyEditor = useTemplateRef('replyEditor');
const messageEditor = useTemplateRef('messageEditor');
const copilot = useCopilotReply();
const shortcutKey = useKbd(['$mod', '+', 'enter']);
@@ -109,6 +110,7 @@ export default {
setQuotedReplyFlagForInbox,
fetchQuotedReplyFlagFromUISettings,
replyEditor,
messageEditor,
copilot,
shortcutKey,
};
@@ -507,7 +509,7 @@ export default {
);
this.fetchAndSetReplyTo();
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.onReplyToMessage);
// A hacky fix to solve the drag and drop
// Is showing on top of new conversation modal drag and drop
@@ -522,7 +524,7 @@ export default {
unmounted() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.onReplyToMessage);
emitter.off(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
emitter.off(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
@@ -1191,6 +1193,15 @@ export default {
return false;
});
},
onReplyToMessage() {
this.fetchAndSetReplyTo();
if (this.inReplyTo) {
this.$nextTick(() => {
const pos = this.isSignatureEnabledForInbox ? 'start' : 'end';
this.messageEditor?.focusEditorInputField(pos);
});
}
},
resetReplyToMessage() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
@@ -1313,6 +1324,7 @@ export default {
/>
<WootMessageEditor
v-else-if="!showAudioRecorderEditor"
ref="messageEditor"
v-model="message"
:conversation-id="conversationId"
:editor-id="editorStateId"
@@ -32,6 +32,7 @@ const emit = defineEmits(['dismiss']);
xs
slate
icon="i-lucide-x"
class="flex-shrink-0"
@click.stop="emit('dismiss')"
/>
</div>
@@ -9,6 +9,7 @@ import { useImageZoom } from 'dashboard/composables/useImageZoom';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import { downloadFile } from '@chatwoot/utils';
import WootModal from 'dashboard/components/Modal.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Avatar from 'next/avatar/Avatar.vue';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
@@ -168,7 +169,7 @@ onMounted(() => {
<template>
<TeleportWithDirection to="body">
<woot-modal
<WootModal
v-model:show="show"
full-width
:show-close-button="false"
@@ -211,7 +212,7 @@ onMounted(() => {
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12 hidden sm:block"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
@@ -353,6 +354,6 @@ onMounted(() => {
</div>
</footer>
</div>
</woot-modal>
</WootModal>
</TeleportWithDirection>
</template>
@@ -2,6 +2,12 @@ import { computed, unref } from 'vue';
import { getCurrentInstance } from 'vue';
export const useStore = () => {
// eslint-disable-next-line no-underscore-dangle
if (window.__CHATWOOT_STORE__) {
// eslint-disable-next-line no-underscore-dangle
return window.__CHATWOOT_STORE__;
}
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
return vm.proxy.$store;
@@ -11,7 +11,12 @@ export function useAccount() {
* Computed property for the current account ID.
* @type {import('vue').ComputedRef<number>}
*/
const route = useRoute();
// In the isolated shell (react-components / Web Component context),
// vue-router is not available. Fall back to the global variable.
// eslint-disable-next-line no-underscore-dangle
const isIsolatedShell = !!window.__WOOT_ISOLATED_SHELL__;
const route = isIsolatedShell ? null : useRoute();
const store = useStore();
const getAccountFn = useMapGetter('accounts/getAccount');
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
@@ -20,6 +25,10 @@ export function useAccount() {
);
const accountId = computed(() => {
if (isIsolatedShell) {
// eslint-disable-next-line no-underscore-dangle
return Number(window.__WOOT_ACCOUNT_ID__);
}
return Number(route.params.accountId);
});
const currentAccount = computed(() => getAccountFn.value(accountId.value));
@@ -10,7 +10,10 @@ const { isImpersonating } = useImpersonation();
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
const { websocketURL = '' } = window.chatwootConfig || {};
super(app, pubsubToken, websocketURL);
// eslint-disable-next-line no-underscore-dangle
const wsURL = websocketURL || window.__WEBSOCKET_URL__ || '';
super(app, pubsubToken, wsURL);
this.CancelTyping = [];
this.events = {
'message.created': this.onMessageCreated,
@@ -98,7 +101,10 @@ class ActionCableConnector extends BaseActionCableConnector {
conversation: { last_activity_at: lastActivityAt },
conversation_id: conversationId,
} = data;
DashboardAudioNotificationHelper.onNewMessage(data);
// eslint-disable-next-line no-underscore-dangle
if (!window.__WOOT_ISOLATED_SHELL__) {
DashboardAudioNotificationHelper.onNewMessage(data);
}
this.app.$store.dispatch('addMessage', data);
this.app.$store.dispatch('updateConversationLastActivity', {
lastActivityAt,
@@ -1,18 +1,6 @@
/* eslint no-param-reassign: 0 */
import getUuid from 'widget/helpers/uuid';
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
export default () => {
if (!Array.prototype.last) {
Object.assign(Array.prototype, {
last() {
return this[this.length - 1];
},
});
}
};
export const isEmptyObject = obj =>
Object.keys(obj).length === 0 && obj.constructor === Object;
@@ -133,20 +133,55 @@ export const ARTICLE_TABS_OPTIONS = [
},
];
export const LOCALE_MENU_ITEMS = [
{
export const LOCALE_MENU_ITEMS = {
makeDefault: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MAKE_DEFAULT',
action: 'change-default',
value: 'default',
icon: 'i-lucide-star',
},
{
moveToDraft: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MOVE_TO_DRAFT',
action: 'move-to-draft',
value: 'draft',
icon: 'i-lucide-eye-off',
},
publishLocale: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.PUBLISH_LOCALE',
action: 'publish-locale',
value: 'publish',
icon: 'i-lucide-eye',
},
delete: {
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE',
action: 'delete',
value: 'delete',
icon: 'i-lucide-trash',
},
];
};
const disableLocaleMenuItems = menuItems =>
menuItems.map(item => ({ ...item, disabled: true }));
export const buildLocaleMenuItems = ({ isDefault, isDraft }) => {
if (isDefault) {
return disableLocaleMenuItems([
LOCALE_MENU_ITEMS.makeDefault,
LOCALE_MENU_ITEMS.moveToDraft,
LOCALE_MENU_ITEMS.delete,
]);
}
if (isDraft) {
return [LOCALE_MENU_ITEMS.publishLocale, LOCALE_MENU_ITEMS.delete];
}
return [
LOCALE_MENU_ITEMS.makeDefault,
LOCALE_MENU_ITEMS.moveToDraft,
LOCALE_MENU_ITEMS.delete,
];
};
export const ARTICLE_EDITOR_STATUS_OPTIONS = {
published: ['archive', 'draft'],
@@ -1,4 +1,8 @@
import { buildPortalArticleURL, buildPortalURL } from '../portalHelper';
import {
buildLocaleMenuItems,
buildPortalArticleURL,
buildPortalURL,
} from '../portalHelper';
describe('PortalHelper', () => {
describe('buildPortalURL', () => {
@@ -68,4 +72,39 @@ describe('PortalHelper', () => {
).toEqual('https://app.chatwoot.com/hc/handbook/articles/article-slug');
});
});
describe('buildLocaleMenuItems', () => {
it('returns disabled actions for the default locale', () => {
expect(
buildLocaleMenuItems({
isDefault: true,
isDraft: false,
})
).toEqual(
expect.arrayContaining([
expect.objectContaining({ action: 'change-default', disabled: true }),
expect.objectContaining({ action: 'move-to-draft', disabled: true }),
expect.objectContaining({ action: 'delete', disabled: true }),
])
);
});
it('returns publish and delete actions for draft locales', () => {
expect(
buildLocaleMenuItems({
isDefault: false,
isDraft: true,
}).map(({ action }) => action)
).toEqual(['publish-locale', 'delete']);
});
it('returns default, draft, and delete actions for live locales', () => {
expect(
buildLocaleMenuItems({
isDefault: false,
isDraft: false,
}).map(({ action }) => action)
).toEqual(['change-default', 'move-to-draft', 'delete']);
});
});
});
@@ -4,6 +4,9 @@
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"COUNT": "{n} bot | {n} bots",
"SEARCH_PLACEHOLDER": "Search bots...",
"NO_RESULTS": "No bots found matching your search",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
@@ -34,7 +37,8 @@
"LOADING": "Fetching bots...",
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "Webhook URL"
"URL": "Webhook URL",
"ACTIONS": "Actions"
}
},
"DELETE": {
@@ -9,6 +9,7 @@
"ADMINISTRATOR": "Administrator",
"AGENT": "Agent"
},
"COUNT": "{n} agent | {n} agents",
"LIST": {
"404": "There are no agents associated to this account",
"TITLE": "Manage agents in your team",
@@ -96,6 +97,8 @@
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"SEARCH_PLACEHOLDER": "Search agents...",
"NO_RESULTS": "No agents found matching your search",
"SEARCH": {
"NO_RESULTS": "No results found."
},
@@ -5,6 +5,9 @@
"LOADING": "Fetching custom attributes",
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
"LEARN_MORE": "Learn more about custom attributes",
"COUNT": "{n} attribute | {n} attributes",
"SEARCH_PLACEHOLDER": "Search attributes...",
"NO_RESULTS": "No attributes found matching your search",
"ATTRIBUTE_MODELS": {
"CONVERSATION": "Conversation",
"CONTACT": "Contact"
@@ -63,6 +66,10 @@
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
},
"BADGES": {
"PRE_CHAT": "Pre-chat",
"RESOLUTION": "Resolution"
}
},
"API": {
@@ -3,8 +3,11 @@
"HEADER": "Automation",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Add Automation Rule",
"COUNT": "{n} automation | {n} automations",
"HEADER_BTN_TXT": "Create Automation",
"LOADING": "Fetching automation rules",
"SEARCH_PLACEHOLDER": "Search automation rules...",
"NO_RESULTS": "No automation rules found matching your search",
"ADD": {
"TITLE": "Add Automation Rule",
"SUBMIT": "Create",
@@ -42,9 +45,9 @@
"LIST": {
"TABLE_HEADER": {
"NAME": "Name",
"DESCRIPTION": "Description",
"ACTIVE": "Active",
"CREATED_ON": "Created on"
"CREATED_ON": "Created on",
"ACTIONS": "Actions"
},
"404": "No automation rules found"
},
@@ -150,7 +153,8 @@
"ADD_PRIVATE_NOTE": "Add a Private Note",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA",
"OPEN_CONVERSATION": "Open conversation"
"OPEN_CONVERSATION": "Open conversation",
"PENDING_CONVERSATION": "Mark conversation as pending"
},
"MESSAGE_TYPES": {
"INCOMING": "Incoming Message",
@@ -22,6 +22,10 @@
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations. Please try again."
},
"RESOLVE": {
"ALL_MISSING_ATTRIBUTES": "Cannot resolve conversations due to missing required attributes",
"PARTIAL_SUCCESS": "Some conversations need required attributes before resolving and were skipped"
},
"LABELS": {
"ASSIGN_LABELS": "Assign labels",
"NO_LABELS_FOUND": "No labels found",
@@ -3,8 +3,11 @@
"HEADER": "Canned Responses",
"LEARN_MORE": "Learn more about canned responses",
"DESCRIPTION": "Canned Responses are pre-written reply templates that help you quickly respond to a conversation. Agents can type the '/' character followed by the shortcode to insert a canned response during a conversation. ",
"COUNT": "{n} canned response | {n} canned responses",
"HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching canned responses...",
"SEARCH_PLACEHOLDER": "Search canned responses...",
"NO_RESULTS": "No canned responses found matching your search",
"SEARCH_404": "There are no items matching this query.",
"LIST": {
"404": "There are no canned responses available in this account.",
@@ -76,6 +76,9 @@
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
},
"priority_desc_created_at_asc": {
"TEXT": "Priority: Highest first, Created: Oldest first"
}
},
"ATTACHMENTS": {
@@ -457,6 +457,9 @@
"INSTAGRAM": {
"PLACEHOLDER": "Add Instagram"
},
"TELEGRAM": {
"PLACEHOLDER": "Add Telegram"
},
"TIKTOK": {
"PLACEHOLDER": "Add TikTok"
},
@@ -573,7 +576,8 @@
"SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search 🔍",
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
"ACTIVE_EMPTY_STATE_TITLE": "በአሁኑ ጊዜ ንቁ እውቂያዎች የሉም 🌙"
}
},
"LOAD_MORE": "Load more"
},
"CONTACTS_BULK_ACTIONS": {
"ASSIGN_LABELS": "Assign Labels",
@@ -607,7 +611,7 @@
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
"CONTACT_SELECTOR": {
"LABEL": "To:",
"TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
"TAG_INPUT_PLACEHOLDER": "Enter at least 2 characters to search by name, email, or phone number",
"CONTACT_CREATING": "Creating contact..."
},
"INBOX_SELECTOR": {
@@ -618,9 +622,9 @@
"SUBJECT_LABEL": "Subject :",
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
"CC_LABEL": "Cc:",
"CC_PLACEHOLDER": "Search for a contact with their email address",
"CC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_LABEL": "Bcc:",
"BCC_PLACEHOLDER": "Search for a contact with their email address",
"BCC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_BUTTON": "Bcc"
},
"MESSAGE_EDITOR": {
@@ -61,6 +61,7 @@
"UNSUPPORTED_MESSAGE": "This message is unsupported. You can view this message on the Facebook / Instagram app.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"UNSUPPORTED_MESSAGE_TIKTOK": "This message is unsupported. You can view this message on the TikTok app.",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
"NO_RESPONSE": "No response",
@@ -173,6 +174,10 @@
"SUCCESFUL": "Assigned label #{labelName} to conversation id {conversationId}",
"FAILED": "Couldn't assign label. Please try again."
},
"LABEL_REMOVAL": {
"SUCCESFUL": "Removed label #{labelName} from conversation id {conversationId}",
"FAILED": "Couldn't remove label. Please try again."
},
"TEAM_ASSIGNMENT": {
"SUCCESFUL": "Assigned team \"{team}\" to conversation id {conversationId}",
"FAILED": "Couldn't assign team. Please try again."
@@ -185,7 +190,11 @@
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
"MESSAGING_RESTRICTED": "You cannot reply to this conversation",
"MESSAGING_RESTRICTED_WHATSAPP": "You can only reply using a template message due to 24-hour message window restriction",
"MESSAGING_RESTRICTED_API": "You can only reply using a template message due to message window restriction",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
"COPILOT_MSG_INPUT": "Give copilot additional prompts, or ask anything else... Press enter to send follow-up",
"CLICK_HERE": "Click here to update",
"WHATSAPP_TEMPLATES": "Whatsapp Templates"
},
@@ -205,7 +214,7 @@
"DRAG_DROP": "Drag and drop here to attach",
"START_AUDIO_RECORDING": "Start audio recording",
"STOP_AUDIO_RECORDING": "Stop audio recording",
"": "",
"COPILOT_THINKING": "Copilot is thinking",
"EMAIL_HEAD": {
"TO": "TO",
"ADD_BCC": "Add bcc",
@@ -247,9 +256,12 @@
"SUCCESS_DELETE_CONVERSATION": "ውይይት በተሳካ ሁኔታ ተሰርዟል",
"FAIL_DELETE_CONVERSATION": "ውይይትን መሰረዝ አልተቻለም! እንደገና ይሞክሩ",
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE} MB attachment limit",
"FILE_TYPE_NOT_SUPPORTED": "This {fileName} file type is not supported in this conversation",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Sent by:",
"BOT": "Bot",
"NATIVE_APP": "Native app",
"NATIVE_APP_ADVISORY": "This message was sent from the native app. Reply from Chatwoot to maintain the message window.",
"SEND_FAILED": "Couldn't send message! Try again",
"TRY_AGAIN": "retry",
"ASSIGNMENT": {
@@ -294,6 +306,7 @@
"CANCEL": "Cancel",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
"SEND_EMAIL_ERROR": "There was an error, please try again",
"SEND_EMAIL_PAYMENT_REQUIRED": "Email transcript is not available on your current plan. Please upgrade to use this feature.",
"FORM": {
"SEND_TO_CONTACT": "Send the transcript to the customer",
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
@@ -3,8 +3,11 @@
"HEADER": "Custom Roles",
"LEARN_MORE": "Learn more about custom roles",
"DESCRIPTION": "Custom roles are roles that are created by the account owner or admin. These roles can be assigned to agents to define their access and permissions within the account. Custom roles can be created with specific permissions and access levels to suit the requirements of the organization.",
"COUNT": "{n} custom role | {n} custom roles",
"HEADER_BTN_TXT": "Add custom role",
"LOADING": "Fetching custom roles...",
"SEARCH_PLACEHOLDER": "Search custom roles...",
"NO_RESULTS": "No custom roles found matching your search",
"SEARCH_404": "There are no items matching this query.",
"PAYWALL": {
"TITLE": "Upgrade to create custom roles",
@@ -1,5 +1,8 @@
{
"DATE_PICKER": {
"PREVIOUS_PERIOD": "Previous period",
"NEXT_PERIOD": "Next period",
"WEEK_NUMBER": "Week #{weekNumber}",
"APPLY_BUTTON": "Apply",
"CLEAR_BUTTON": "Clear",
"DATE_RANGE_INPUT": {
@@ -13,6 +16,8 @@
"LAST_3_MONTHS": "Last 3 months",
"LAST_6_MONTHS": "Last 6 months",
"LAST_YEAR": "Last year",
"THIS_WEEK": "This week",
"MONTH_TO_DATE": "This month",
"CUSTOM_RANGE": "Custom date range"
}
}
@@ -7,6 +7,13 @@
},
"CLOSE": "Close",
"BETA": "Beta",
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it.",
"ACCEPT": "Accept",
"DISCARD": "Discard",
"PREFERRED": "Preferred"
},
"CHOICE_TOGGLE": {
"YES": "Yes",
"NO": "No"
}
}
@@ -182,6 +182,7 @@
},
"COMMAND_BAR": {
"SEARCH_PLACEHOLDER": "Search or jump to",
"SNOOZE_PLACEHOLDER": "Type a time e.g. tomorrow, 2 hours, next friday, jan 15...",
"SECTIONS": {
"GENERAL": "General",
"REPORTS": "Reports",
@@ -374,6 +374,16 @@
"ERROR_MESSAGE": "Error while deleting article"
}
},
"REORDER_ARTICLE": {
"API": {
"ERROR_MESSAGE": "Unable to reorder articles. Please try again."
}
},
"REORDER_CATEGORY": {
"API": {
"ERROR_MESSAGE": "Unable to reorder categories. Please try again."
}
},
"CREATE_ARTICLE": {
"ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings"
},
@@ -3,6 +3,9 @@
"HEADER": "Inboxes",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"COUNT": "{n} inbox | {n} inboxes",
"SEARCH_PLACEHOLDER": "Search inboxes...",
"NO_RESULTS": "No inboxes found matching your search",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
@@ -575,7 +578,7 @@
"SUBTITLE": "Use only the configured business name as the sender name in the email header."
},
"BUSINESS_NAME": {
"BUTTON_TEXT": "+ Configure your business name",
"BUTTON_TEXT": "Configure your business name",
"PLACEHOLDER": "Enter your business name",
"SAVE_BUTTON_TEXT": "Save"
}
@@ -589,8 +592,10 @@
"DISABLED": "Disabled"
},
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
"ENABLED": "Reopen same conversation",
"DISABLED": "Create new conversations",
"ENABLED_DESCRIPTION": "When a contact messages again, the previous conversation will be reopened.",
"DISABLED_DESCRIPTION": "A new conversation will be created each time after the previous one is resolved."
},
"ENABLE_HMAC": {
"LABEL": "Enable"
@@ -625,6 +630,8 @@
"ACCOUNT_HEALTH": "Account Health",
"CSAT": "CSAT"
},
"CHANNEL_PREFERENCES": "Channel Preferences",
"WIDGET_FEATURES": "Widget features",
"ACCOUNT_HEALTH": {
"TITLE": "Manage your WhatsApp account",
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
@@ -678,6 +685,16 @@
"SANDBOX": "Sandbox",
"LIVE": "Live"
}
},
"WEBHOOK": {
"TITLE": "Webhook Configuration",
"DESCRIPTION": "Webhook URL is required for your WhatsApp Business Account to receive messages from customers",
"ACTION_REQUIRED": "Webhook not configured",
"REGISTER_BUTTON": "Register Webhook",
"REGISTER_SUCCESS": "Webhook registered successfully",
"REGISTER_ERROR": "Failed to register webhook. Please try again.",
"CONFIGURED_SUCCESS": "Webhook configured successfully",
"URL_MISMATCH": "Webhook URL mismatch"
}
},
"SETTINGS": "Settings",
@@ -694,7 +711,7 @@
"ALLOWED_DOMAINS": {
"TITLE": "Allowed Domains",
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
"PLACEHOLDER": "example.com, www.example.com, app.example.com"
},
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
@@ -708,8 +725,8 @@
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"LOCK_TO_SINGLE_CONVERSATION": "Conversation Routing",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Configure conversation creation for existing contacts",
"INBOX_UPDATE_TITLE": "Inbox Settings",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
@@ -758,6 +775,7 @@
"LABEL": "Help Center",
"PLACEHOLDER": "Select Help Center",
"SELECT_PLACEHOLDER": "Select Help Center",
"NONE": "None",
"REMOVE": "Remove Help Center",
"SUB_TEXT": "Attach a Help Center with the inbox"
},
@@ -766,6 +784,53 @@
"MAX_ASSIGNMENT_LIMIT_RANGE_ERROR": "Please enter a value greater than 0",
"MAX_ASSIGNMENT_LIMIT_SUB_TEXT": "Limit the maximum number of conversations from this inbox that can be auto assigned to an agent"
},
"ASSIGNMENT": {
"TITLE": "Conversation Assignment",
"DESCRIPTION": "Automatically assign incoming conversations to available agents based on assignment policies",
"ENABLE_AUTO_ASSIGNMENT": "Enable automatic conversation assignment",
"DEFAULT_RULES_TITLE": "Default assignment rules",
"DEFAULT_RULES_DESCRIPTION": "Using the default assignment behavior for all conversations",
"DEFAULT_RULE_1": "Earliest created conversations first",
"DEFAULT_RULE_2": "Round robin distribution",
"CUSTOMIZE_WITH_POLICY": "Customize with assignment policy",
"USING_POLICY": "Using custom assignment policy for this inbox",
"CUSTOMIZE_POLICY": "Customize with assignment policy",
"DELETE_POLICY": "Delete policy",
"POLICY_LABEL": "Assignment policy",
"ASSIGNMENT_ORDER_LABEL": "Assignment Order",
"ASSIGNMENT_METHOD_LABEL": "Assignment Method",
"POLICY_STATUS": {
"ACTIVE": "Active",
"INACTIVE": "Inactive"
},
"PRIORITY": {
"EARLIEST_CREATED": "Earliest created",
"LONGEST_WAITING": "Longest waiting"
},
"METHOD": {
"ROUND_ROBIN": "Round robin",
"BALANCED": "Balanced assignment"
},
"UPGRADE_PROMPT": "Custom assignment policies are available on the Business plan",
"UPGRADE_TO_BUSINESS": "Upgrade to Business",
"DEFAULT_POLICY_LINKED": "Default policy linked",
"DEFAULT_POLICY_DESCRIPTION": "Link a custom assignment policy to customize how conversations are assigned to agents in this inbox.",
"LINK_EXISTING_POLICY": "Link existing policy",
"CREATE_NEW_POLICY": "Create new policy",
"NO_POLICIES": "No assignment policies found",
"VIEW_ALL_POLICIES": "View all policies",
"CURRENT_BEHAVIOR": "Currently using default assignment behavior:",
"LINK_SUCCESS": "Assignment policy linked successfully",
"LINK_ERROR": "Failed to link assignment policy"
},
"ASSIGNMENT_POLICY": {
"DELETE_CONFIRM_TITLE": "Delete assignment policy?",
"DELETE_CONFIRM_MESSAGE": "Are you sure you want to remove this assignment policy from this inbox? The inbox will revert to default assignment rules.",
"CANCEL": "Cancel",
"CONFIRM_DELETE": "Delete",
"DELETE_SUCCESS": "Assignment policy removed successfully",
"DELETE_ERROR": "Failed to remove assignment policy"
},
"FACEBOOK_REAUTHORIZE": {
"TITLE": "Reauthorize",
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
@@ -837,6 +902,20 @@
"CONFIRM": "Create new template",
"CANCEL": "Go back"
},
"UTILITY_ANALYZER": {
"ACTION": "Check utility fit",
"HELPER_NOTE": "Check this message before submission to improve Utility fit. The system creates a dedicated CSAT template with buttons for reporting and submits it as Utility; Meta may still reclassify it as Marketing based on content.",
"RESULT_LABEL": "Meta category prediction",
"GUIDANCE_NOTE": "This is a guidance check, not a guarantee of Meta approval.",
"SUGGESTION_LABEL": "Suggested utility-safe rewrite",
"APPLY": "Use this rewrite",
"ERROR_MESSAGE": "Couldn't analyze the message. Please try again.",
"CLASSIFICATION": {
"LIKELY_UTILITY": "Likely Utility",
"LIKELY_MARKETING": "Likely Marketing",
"UNCLEAR": "Needs clarification"
}
},
"SURVEY_RULE": {
"LABEL": "Survey rule",
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
@@ -848,7 +927,7 @@
"SELECT_PLACEHOLDER": "select labels"
},
"NOTE": "Note: CSAT surveys are sent only once per conversation",
"WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
"WHATSAPP_NOTE": "Note: When you save, the system creates a dedicated CSAT template in WhatsApp (used to capture rating and feedback in reports) and submits it as Utility for approval. Meta may still classify it as Marketing based on content. After approval, surveys are sent only once per conversation as per the survey rule.",
"API": {
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
@@ -864,9 +943,11 @@
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours visitors can be warned with a message and a pre-chat form.",
"DAY": {
"DAY": "Day",
"AVAILABILITY": "Availability",
"HOURS": "Hours",
"ENABLE": "Enable availability for this day",
"UNAVAILABLE": "Unavailable",
"HOURS": "hours",
"VALIDATION_ERROR": "Starting time should be before closing time.",
"CHOOSE": "Choose"
},
@@ -973,11 +1054,12 @@
"IN_A_DAY": "In a day"
},
"WIDGET_COLOR_LABEL": "Widget Color",
"WIDGET_BUBBLE_POSITION_LABEL": "Widget Bubble Position",
"WIDGET_BUBBLE_TYPE_LABEL": "Widget Bubble Type",
"WIDGET_BUBBLE": "Bubble",
"WIDGET_BUBBLE_POSITION_LABEL": "Position:",
"WIDGET_BUBBLE_TYPE_LABEL": "Type:",
"WIDGET_BUBBLE_LAUNCHER_TITLE": {
"DEFAULT": "Chat with us",
"LABEL": "Widget Bubble Launcher Title",
"LABEL": "Launcher Title",
"PLACE_HOLDER": "Chat with us"
},
"UPDATE": {
@@ -1002,7 +1084,7 @@
},
"WIDGET_SCREEN": {
"DEFAULT": "Default",
"CHAT": "Chat"
"CHAT": "Chat mode"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "Typically replies in a few minutes",
@@ -3,6 +3,9 @@
"FETCHING": "Fetching Integrations",
"NO_HOOK_CONFIGURED": "There are no {integrationId} integrations configured in this account.",
"HEADER": "Applications",
"COUNT": "{n} integration | {n} integrations",
"SEARCH_PLACEHOLDER": "Search...",
"NO_RESULTS": "No results found matching your search",
"STATUS": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
@@ -31,6 +34,7 @@
"LIST": {
"FETCHING": "Fetching integration hooks",
"INBOX": "Inbox",
"ACTIONS": "Actions",
"DELETE": {
"BUTTON_TEXT": "Delete"
}
@@ -1,6 +1,7 @@
{
"INTEGRATION_SETTINGS": {
"SHOPIFY": {
"HEADER": "Shopify",
"DELETE": {
"TITLE": "Delete Shopify Integration",
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
@@ -19,6 +20,8 @@
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
"LEARN_MORE": "Learn more about integrations",
"LOADING": "Fetching integrations",
"SEARCH_PLACEHOLDER": "Search integrations...",
"NO_RESULTS": "No integrations found matching your search",
"CAPTAIN": {
"DISABLED": "Captain is not enabled on your account.",
"CLICK_HERE_TO_CONFIGURE": "Click here to configure",
@@ -28,6 +31,17 @@
"WEBHOOK": {
"SUBSCRIBED_EVENTS": "Subscribed Events",
"LEARN_MORE": "Learn more about webhooks",
"SECRET": {
"LABEL": "Secret",
"COPY": "Copy secret to clipboard",
"COPY_SUCCESS": "Secret copied to clipboard",
"TOGGLE": "Toggle secret visibility",
"CREATED_DESC": "Your webhook has been created. Use the secret below to verify webhook signatures. Please copy it now — you can also find it later in the webhook edit form.",
"DONE": "Done"
},
"COUNT": "{n} webhook | {n} webhooks",
"SEARCH_PLACEHOLDER": "Search webhooks...",
"NO_RESULTS": "No webhooks found matching your search",
"FORM": {
"CANCEL": "Cancel",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
@@ -104,6 +118,7 @@
}
},
"SLACK": {
"HEADER": "Slack",
"DELETE": "Delete",
"DELETE_CONFIRMATION": {
"TITLE": "Delete the integration",
@@ -145,7 +160,29 @@
"EXPAND": "Expand",
"MAKE_FRIENDLY": "Change message tone to friendly",
"MAKE_FORMAL": "Use formal tone",
"SIMPLIFY": "Simplify"
"SIMPLIFY": "Simplify",
"CONFIDENT": "Use confident tone",
"PROFESSIONAL": "Use professional tone",
"CASUAL": "Use casual tone",
"STRAIGHTFORWARD": "Use straightforward tone"
},
"REPLY_OPTIONS": {
"IMPROVE_REPLY": "Improve reply",
"IMPROVE_REPLY_SELECTION": "Improve the selection",
"CHANGE_TONE": {
"TITLE": "Change tone",
"OPTIONS": {
"PROFESSIONAL": "Professional",
"CASUAL": "Casual",
"STRAIGHTFORWARD": "Straightforward",
"CONFIDENT": "Confident",
"FRIENDLY": "Friendly"
}
},
"GRAMMAR": "Fix grammar & spelling",
"SUGGESTION": "Suggest a reply",
"SUMMARIZE": "Summarize the conversation",
"ASK_COPILOT": "Ask Copilot"
},
"ASSISTANCE_MODAL": {
"DRAFT_TITLE": "Draft content",
@@ -201,12 +238,16 @@
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.",
"LEARN_MORE": "Learn more about Dashboard Apps",
"COUNT": "{n} dashboard app | {n} dashboard apps",
"SEARCH_PLACEHOLDER": "Search dashboard apps...",
"NO_RESULTS": "No dashboard apps found matching your search",
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",
"TABLE_HEADER": {
"NAME": "Name",
"ENDPOINT": "Endpoint"
"ENDPOINT": "Endpoint",
"ACTIONS": "Actions"
},
"EDIT_TOOLTIP": "Edit app",
"DELETE_TOOLTIP": "Delete app"
@@ -243,6 +284,7 @@
}
},
"LINEAR": {
"HEADER": "Linear",
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "Fetching linear issues...",
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
@@ -337,6 +379,7 @@
}
},
"NOTION": {
"HEADER": "Notion",
"DELETE": {
"TITLE": "Are you sure you want to delete the Notion integration?",
"MESSAGE": "Deleting this integration will remove access to your Notion workspace and stop all related functionality.",
@@ -406,6 +449,7 @@
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain AI is only available in the Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
@@ -5,6 +5,9 @@
"LOADING": "Fetching labels",
"DESCRIPTION": "Labels help you categorize and prioritize conversations and leads. You can assign a label to a conversation or contact using the side panel.",
"LEARN_MORE": "Learn more about labels",
"COUNT": "{n} label | {n} labels",
"SEARCH_PLACEHOLDER": "Search labels...",
"NO_RESULTS": "No labels found matching your search",
"SEARCH_404": "There are no items matching this query",
"LIST": {
"404": "There are no labels available in this account.",
@@ -13,7 +16,8 @@
"TABLE_HEADER": {
"NAME": "Name",
"DESCRIPTION": "Description",
"COLOR": "Color"
"COLOR": "Color",
"ACTION": "Actions"
}
},
"FORM": {
@@ -3,9 +3,12 @@
"HEADER": "Macros",
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
"LEARN_MORE": "Learn more about macros",
"COUNT": "{n} macro | {n} macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SEARCH_PLACEHOLDER": "Search macros...",
"NO_RESULTS": "No macros found matching your search",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"ADD": {
@@ -29,7 +32,8 @@
"NAME": "Name",
"CREATED BY": "Created by",
"LAST_UPDATED_BY": "Last updated by",
"VISIBILITY": "Visibility"
"VISIBILITY": "Visibility",
"ACTIONS": "Actions"
},
"404": "No macros found"
},
@@ -1,7 +1,7 @@
{
"MFA_SETTINGS": {
"TITLE": "Two-Factor Authentication",
"SUBTITLE": "Secure your account with TOTP-based authentication",
"SUBTITLE": "Protect your account from unauthorized access with TOTP-based authentication. This adds an extra layer of security to your account.",
"DESCRIPTION": "Add an extra layer of security to your account using a time-based one-time password (TOTP)",
"STATUS_TITLE": "Authentication Status",
"STATUS_DESCRIPTION": "Manage your two-factor authentication settings and backup recovery codes",
@@ -3,7 +3,7 @@
"HEADER": "Conversations",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"DOWNLOAD_CONVERSATION_REPORTS": "Download conversation reports",
"DATA_FETCHING_FAILED": "Failed to fetch data, please try again later.",
"SUMMARY_FETCHING_FAILED": "Failed to fetch summary, please try again later.",
"METRICS": {
@@ -128,11 +128,16 @@
},
"AGENT_REPORTS": {
"HEADER": "Agents Overview",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent’s name to learn more.",
"DESCRIPTION": "Easily track agent performance with key metrics such as conversations, response times, resolution times, and resolved cases. Click an agent's name to learn more.",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"FILTER_DROPDOWN_LABEL": "Select Agent",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"AGENTS": "Search agents"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -201,6 +206,11 @@
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
"FILTER_DROPDOWN_LABEL": "Select Label",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"LABELS": "Search labels"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -271,6 +281,11 @@
"FILTER_DROPDOWN_LABEL": "Select Inbox",
"ALL_INBOXES": "All Inboxes",
"SEARCH_INBOX": "Search Inbox",
"FILTERS": {
"INPUT_PLACEHOLDER": {
"INBOXES": "Search inboxes"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -334,11 +349,19 @@
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"DESCRIPTION": "Get a snapshot of your team’s performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"DESCRIPTION": "Get a snapshot of your team's performance with essential metrics, including conversations, response times, resolution times, and resolved cases. Click a team name for more details.",
"LOADING_CHART": "Loading chart data...",
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"FILTERS": {
"ADD_FILTER": "Add filter",
"CLEAR_ALL": "Clear all",
"NO_FILTER": "No filters available",
"INPUT_PLACEHOLDER": {
"TEAMS": "Search teams"
}
},
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversations",
@@ -402,22 +425,48 @@
},
"CSAT_REPORTS": {
"HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.",
"NO_RECORDS": "No responses yet",
"NO_RECORDS_DESCRIPTION": "CSAT survey responses will appear here once customers start providing feedback.",
"DOWNLOAD": "Download CSAT Reports",
"DOWNLOAD_FAILED": "Failed to download CSAT Reports",
"FILTERS": {
"ADD_FILTER": "Add filter",
"CLEAR_ALL": "Clear all",
"NO_FILTER": "No filters available",
"INPUT_PLACEHOLDER": {
"AGENTS": "Search agents",
"INBOXES": "Search inboxes",
"TEAMS": "Search teams",
"RATINGS": "Search ratings"
},
"AGENTS": {
"PLACEHOLDER": "Choose Agents"
"LABEL": "Agent"
},
"INBOXES": {
"LABEL": "Inbox"
},
"TEAMS": {
"LABEL": "Team"
},
"RATINGS": {
"LABEL": "Rating"
}
},
"TABLE": {
"HEADER": {
"CONTACT_NAME": "Contact",
"AGENT_NAME": "Assigned agent",
"AGENT_NAME": "Agent",
"RATING": "Rating",
"FEEDBACK_TEXT": "Feedback comment"
}
"FEEDBACK_TEXT": "Feedback comment",
"CONVERSATION": "Conversation",
"CUSTOMER": "Customer",
"RESPONSE": "Response",
"HANDLED_BY": "Handled by"
},
"UNKNOWN_CUSTOMER": "Unknown customer"
},
"NO_AGENT": "No assigned agent",
"NO_FEEDBACK": "No feedback provided",
"METRIC": {
"TOTAL_RESPONSES": {
"LABEL": "Total responses",
@@ -430,6 +479,25 @@
"RESPONSE_RATE": {
"LABEL": "Response rate",
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
},
"RATING_DISTRIBUTION": "Rating distribution"
},
"REVIEW_NOTES": {
"TITLE": "Review notes",
"PLACEHOLDER": "Add review notes about this rating...",
"SAVE": "Save",
"CANCEL": "Cancel",
"SAVING": "Saving...",
"SAVED": "Notes saved successfully",
"SAVE_ERROR": "Failed to save notes",
"UPDATED_BY": "Updated by {name} {time}",
"UPDATED_BY_LABEL": "Updated by",
"PAYWALL": {
"TITLE": "Upgrade to add review notes",
"AVAILABLE_ON": "The review notes feature is only available in the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Add internal context to every CSAT response with review notes. Capture what really happened, spot patterns faster, and make better decisions from your feedback.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
}
}
},
@@ -273,7 +273,8 @@
"FILE_BUBBLE": {
"DOWNLOAD": "Download",
"UPLOADING": "Uploading...",
"INSTAGRAM_STORY_UNAVAILABLE": "This story is no longer available."
"INSTAGRAM_STORY_UNAVAILABLE": "This story is no longer available.",
"INSTAGRAM_STORY_REPLY": "Replied to your story:"
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
@@ -378,7 +379,57 @@
"INFO_SHORT": "Automatically mark offline when you aren't using the app."
},
"DOCS": "Read docs",
"SECURITY": "Security"
"SECURITY": "Security",
"CAPTAIN_AI": "Captain",
"CONVERSATION_WORKFLOW": "Conversation Workflow"
},
"CAPTAIN_SETTINGS": {
"TITLE": "Captain Settings",
"DESCRIPTION": "Configure your AI models and features for Captain. Captain follows a credit based billing, you will be charged credits for every action Captain takes based on the model selected.",
"LOADING": "Loading Captain configuration...",
"LINK_TEXT": "Learn more about Captain Credits",
"NOT_ENABLED": "Captain is not enabled for your account. Please upgrade your plan to access Captain features.",
"MODEL_CONFIG": {
"TITLE": "Model Configuration",
"DESCRIPTION": "Select AI models for different features.",
"SELECT_MODEL": "Select model",
"CREDITS_PER_MESSAGE": "{credits} credit/message",
"COMING_SOON": "Coming soon",
"EDITOR": {
"TITLE": "Editor Features",
"DESCRIPTION": "Powers smart compose, grammar corrections, tone adjustments, and content enhancement in your message editor."
},
"ASSISTANT": {
"TITLE": "Assistant",
"DESCRIPTION": "Handles automated responses, conversation summaries, and intelligent reply suggestions for customer interactions."
},
"COPILOT": {
"TITLE": "Co-pilot",
"DESCRIPTION": "Provides real-time contextual suggestions, knowledge base recommendations, and proactive insights during conversations."
}
},
"FEATURES": {
"TITLE": "Features",
"DESCRIPTION": "Enable or disable AI-powered features.",
"AUDIO_TRANSCRIPTION": {
"TITLE": "Audio Transcription",
"DESCRIPTION": "Automatically convert voice messages and call recordings into searchable text transcripts."
},
"HELP_CENTER_SEARCH": {
"TITLE": "Help Center Search Indexing",
"DESCRIPTION": "Use AI for context aware search inside your help center articles."
},
"LABEL_SUGGESTION": {
"TITLE": "Label Suggestion",
"DESCRIPTION": "Automatically suggest relevant labels and tags for conversations based on content analysis and context.",
"MODEL_TITLE": "Label Suggestion Model",
"MODEL_DESCRIPTION": "Select the AI model to use for analyzing conversations and suggesting appropriate labels"
}
},
"API": {
"SUCCESS": "Captain settings updated successfully.",
"ERROR": "Failed to update Captain settings. Please try again."
}
},
"BILLING_SETTINGS": {
"TITLE": "Billing",
@@ -506,6 +557,58 @@
}
}
},
"CONVERSATION_WORKFLOW": {
"INDEX": {
"HEADER": {
"TITLE": "Conversation Workflows",
"DESCRIPTION": "Configure rules and required fields for conversation resolution."
}
},
"REQUIRED_ATTRIBUTES": {
"TITLE": "Attributes required on resolution",
"DESCRIPTION": "When resolving a conversation, agents will be prompted to fill these attributes if they haven't yet.",
"NO_ATTRIBUTES": "No attributes added yet",
"ADD": {
"TITLE": "Add Attributes",
"SEARCH_PLACEHOLDER": "Search attributes"
},
"SAVE": {
"SUCCESS": "Required attributes updated",
"ERROR": "Could not update required attributes, please try again"
},
"MODAL": {
"TITLE": "Resolve conversation",
"DESCRIPTION": "Please fill in the following custom attributes before resolving this conversation",
"ACTIONS": {
"RESOLVE": "Resolve conversation",
"CANCEL": "Cancel"
},
"PLACEHOLDERS": {
"TEXT": "Write a note...",
"NUMBER": "Enter a number",
"LINK": "Add a link",
"DATE": "Pick a date",
"LIST": "Select an option"
},
"CHECKBOX": {
"YES": "Yes",
"NO": "No"
}
},
"PAYWALL": {
"TITLE": "Upgrade to use required attributes",
"AVAILABLE_ON": "The required conversation attributes feature is available on the Business and Enterprise plans.",
"UPGRADE_PROMPT": "Upgrade your plan to prompt agents to fill required attributes before conversation resolution.",
"UPGRADE_NOW": "Upgrade now",
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "The required conversation attributes feature is available on the paid plans.",
"UPGRADE_PROMPT": "Upgrade to a paid plan to enforce required attributes before conversation resolution.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
}
}
},
"CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
"NEW_ACCOUNT": "New Account",
@@ -591,7 +694,8 @@
"CREATE_BUTTON": "Create policy",
"API": {
"SUCCESS_MESSAGE": "Assignment policy created successfully",
"ERROR_MESSAGE": "Failed to create assignment policy"
"ERROR_MESSAGE": "Failed to create assignment policy",
"INBOX_LINKED": "Inbox has been linked to the policy"
}
},
"EDIT": {
@@ -605,6 +709,12 @@
"CONFIRM_BUTTON_LABEL": "Continue",
"CANCEL_BUTTON_LABEL": "Cancel"
},
"INBOX_LINK_PROMPT": {
"TITLE": "Link inbox to policy",
"DESCRIPTION": "Would you like to link this inbox to the assignment policy?",
"LINK_BUTTON": "Link inbox",
"CANCEL_BUTTON": "Skip"
},
"API": {
"SUCCESS_MESSAGE": "Assignment policy updated successfully",
"ERROR_MESSAGE": "Failed to update assignment policy"
@@ -643,7 +753,9 @@
},
"BALANCED": {
"LABEL": "Balanced",
"DESCRIPTION": "Assign conversations based on available capacity."
"DESCRIPTION": "Assign conversations based on available capacity.",
"PREMIUM_MESSAGE": "Upgrade to access balanced assignment and agent capacity management.",
"PREMIUM_BADGE": "Premium"
}
},
"ASSIGNMENT_PRIORITY": {
@@ -729,6 +841,20 @@
"SUCCESS_MESSAGE": "Agent removed from policy successfully",
"ERROR_MESSAGE": "Failed to remove agent from policy"
}
},
"INBOX_LIMIT_API": {
"ADD": {
"SUCCESS_MESSAGE": "Inbox limit added successfully",
"ERROR_MESSAGE": "Failed to add inbox limit"
},
"UPDATE": {
"SUCCESS_MESSAGE": "Inbox limit updated successfully",
"ERROR_MESSAGE": "Failed to update inbox limit"
},
"DELETE": {
"SUCCESS_MESSAGE": "Inbox limit deleted successfully",
"ERROR_MESSAGE": "Failed to delete inbox limit"
}
}
},
"FORM": {
@@ -1,6 +1,7 @@
{
"REGISTER": {
"TRY_WOOT": "Create an account",
"GET_STARTED": "Get started with Chatwoot",
"TITLE": "Register",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
@@ -5,7 +5,12 @@
"ADD_ACTION_LONG": "Create a new SLA Policy",
"DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.",
"LEARN_MORE": "Learn more about SLA",
"COUNT": "{n} SLA | {n} SLAs",
"LOADING": "Fetching SLAs",
"SEARCH_PLACEHOLDER": "Search SLA...",
"SEARCH": {
"NO_RESULTS": "No SLA found matching your search"
},
"PAYWALL": {
"TITLE": "Upgrade to create SLAs",
"AVAILABLE_ON": "The SLA feature is only available in the Business and Enterprise plans.",
@@ -20,14 +25,18 @@
},
"LIST": {
"404": "There are no SLAs available in this account.",
"TABLE_HEADER": {
"SLA": "SLA",
"BUSINESS_HOURS": "Business hours"
},
"EMPTY": {
"TITLE_1": "Enterprise P0",
"DESC_1": "Issues raised by enterprise customers, that require immediate attention.",
"TITLE_2": "Enterprise P1",
"DESC_2": "Issues raised by enterprise customers, that needs to be acknowledged quickly."
},
"BUSINESS_HOURS_ON": "Business hours on",
"BUSINESS_HOURS_OFF": "Business hours off",
"BUSINESS_HOURS_ON": "Turned on",
"BUSINESS_HOURS_OFF": "Turned off",
"RESPONSE_TYPES": {
"FRT": "First response time threshold",
"NRT": "Next response time threshold",
@@ -0,0 +1,72 @@
{
"SNOOZE_PARSER": {
"UNITS": {
"MINUTE": "minute",
"MINUTES": "minutes",
"HOUR": "hour",
"HOURS": "hours",
"DAY": "day",
"DAYS": "days",
"WEEK": "week",
"WEEKS": "weeks",
"MONTH": "month",
"MONTHS": "months",
"YEAR": "year",
"YEARS": "years"
},
"HALF": "half",
"NEXT": "next",
"THIS": "this",
"AT": "at",
"IN": "in",
"FROM_NOW": "from now",
"NEXT_YEAR": "next year",
"MERIDIEM": {
"AM": "am",
"PM": "pm"
},
"RELATIVE": {
"TOMORROW": "tomorrow",
"DAY_AFTER_TOMORROW": "day after tomorrow",
"NEXT_WEEK": "next week",
"NEXT_MONTH": "next month",
"THIS_WEEKEND": "this weekend",
"NEXT_WEEKEND": "next weekend"
},
"TIME_OF_DAY": {
"MORNING": "morning",
"AFTERNOON": "afternoon",
"EVENING": "evening",
"NIGHT": "night",
"NOON": "noon",
"MIDNIGHT": "midnight"
},
"WORD_NUMBERS": {
"ONE": "one",
"TWO": "two",
"THREE": "three",
"FOUR": "four",
"FIVE": "five",
"SIX": "six",
"SEVEN": "seven",
"EIGHT": "eight",
"NINE": "nine",
"TEN": "ten",
"TWELVE": "twelve",
"FIFTEEN": "fifteen",
"TWENTY": "twenty",
"THIRTY": "thirty"
},
"ORDINALS": {
"FIRST": "first",
"SECOND": "second",
"THIRD": "third",
"FOURTH": "fourth",
"FIFTH": "fifth"
},
"OF": "of",
"AFTER": "after",
"WEEK": "week",
"DAY": "day"
}
}
@@ -5,6 +5,9 @@
"LOADING": "Fetching teams",
"DESCRIPTION": "Teams allow you to organize agents into groups based on their responsibilities. An agent can belong to multiple teams. When working collaboratively, you can assign conversations to specific teams.",
"LEARN_MORE": "Learn more about teams",
"COUNT": "{n} team | {n} teams",
"SEARCH_PLACEHOLDER": "Search teams...",
"NO_RESULTS": "No teams found matching your search",
"LIST": {
"404": "There are no teams created on this account.",
"EDIT_TEAM": "Edit team",
@@ -64,8 +67,8 @@
"ERROR_MESSAGE": "Couldn't save the team details. Try again."
},
"AGENTS": {
"AGENT": "AGENT",
"EMAIL": "EMAIL",
"AGENT": "Agent",
"EMAIL": "Email",
"BUTTON_TEXT": "Add agents",
"ADD_AGENTS": "Adding Agents to your Team...",
"SELECT": "select",
@@ -4,6 +4,9 @@
"LOADING_EDITOR": "جار جلب المحرر...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"LEARN_MORE": "Learn about agent bots",
"COUNT": "{n} bot | {n} bots",
"SEARCH_PLACEHOLDER": "Search bots...",
"NO_RESULTS": "No bots found matching your search",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT_BADGE": "النظام",
"AVATAR": {
@@ -34,7 +37,8 @@
"LOADING": "جار جلب الروبوتات...",
"TABLE_HEADER": {
"DETAILS": "Bot Details",
"URL": "رابط Webhook"
"URL": "رابط Webhook",
"ACTIONS": "الإجراءات"
}
},
"DELETE": {
@@ -9,6 +9,7 @@
"ADMINISTRATOR": "المدير",
"AGENT": "وكيل الدعم"
},
"COUNT": "{n} وكيل | {n} وكلاء",
"LIST": {
"404": "لا يوجد وكلاء دعم مرتبطين بهذا الحساب",
"TITLE": "إدارة وكلاء الدعم في فريقك",
@@ -96,6 +97,8 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"SEARCH_PLACEHOLDER": "البحث عن وكلاء...",
"NO_RESULTS": "No agents found matching your search",
"SEARCH": {
"NO_RESULTS": "لم يتم العثور على النتائج."
},
@@ -5,6 +5,9 @@
"LOADING": "جلب الصفات المخصصة",
"DESCRIPTION": "سمة مخصصة تتتبع تفاصيل إضافية حول جهات الاتصال أو المحادثات الخاصة بك - مثل خطة الاشتراك أو تاريخ الشراء الأول. يمكنك إضافة أنواع مختلفة من السمات المخصصة، مثل النص أو القوائم أو الأرقام، لالتقاط المعلومات المحددة التي تحتاجها.",
"LEARN_MORE": "تعرف على المزيد حول السمات المخصصة",
"COUNT": "{n} attribute | {n} attributes",
"SEARCH_PLACEHOLDER": "البحث عن صفات...",
"NO_RESULTS": "No attributes found matching your search",
"ATTRIBUTE_MODELS": {
"CONVERSATION": "المحادثات",
"CONTACT": "جهات الاتصال"
@@ -63,6 +66,10 @@
},
"ENABLE_REGEX": {
"LABEL": "تمكين التحقق من صحة regex"
},
"BADGES": {
"PRE_CHAT": "Pre-chat",
"RESOLUTION": "Resolution"
}
},
"API": {
@@ -23,52 +23,52 @@
},
"DEFAULT_USER": "النظام",
"AUTOMATION_RULE": {
"ADD": "{agentName} أنشأ قاعدة أتمتة جديدة (##{id})",
"EDIT": "{agentName} قام بتحديث قاعدة أتمتة (##{id})",
"DELETE": "{agentName} حذف قاعدة أتمتة (##{id})"
"ADD": "{agentName} أنشأ قاعدة أتمتة جديدة (#{id})",
"EDIT": "{agentName} قام بتحديث قاعدة أتمتة (#{id})",
"DELETE": "{agentName} حذف قاعدة أتمتة (#{id})"
},
"ACCOUNT_USER": {
"ADD": "{agentName} دعا {invitee} إلى الحساب كـ {role}",
"EDIT": {
"SELF": "{agentName} غير {attributes} الخاصة به إلى {values}",
"OTHER": "{agentName} غير {attributes} لـ {user} إلى {values}",
"DELETED": "{agentName} غير {attributes} لـ %{user} إلى {values}"
"DELETED": "{agentName} غير {attributes} للمستخدم المحذوف إلى {values}"
}
},
"INBOX": {
"ADD": "{agentName} أنشأ صندوق وارد جديد (##{id})",
"EDIT": "{agentName} قام بتحديث صندوق الوارد (##{id})",
"DELETE": "{agentName} حذف صندوق الوارد (##{id})"
"ADD": "{agentName} أنشأ صندوق وارد جديد (#{id})",
"EDIT": "{agentName} قام بتحديث صندوق الوارد (#{id})",
"DELETE": "{agentName} حذف صندوق الوارد (#{id})"
},
"WEBHOOK": {
"ADD": "{agentName} أنشأ Webhook جديد (##{id})",
"EDIT": "{agentName} قام بتحديث Webhook (##{id})",
"DELETE": "{agentName} حذف Webhook (##{id})"
"ADD": "{agentName} أنشأ Webhook جديد (#{id})",
"EDIT": "{agentName} قام بتحديث Webhook (#{id})",
"DELETE": "{agentName} حذف Webhook (#{id})"
},
"USER_ACTION": {
"SIGN_IN": "{agentName} قام بتسجيل الدخول",
"SIGN_OUT": "{agentName} قام بتسجيل الخروج"
},
"TEAM": {
"ADD": "{agentName} أنشأ فريق جديد (##{id})",
"EDIT": "{agentName} قام بتحديث الفريق (##{id})",
"DELETE": "{agentName} حذف الفريق (##{id})"
"ADD": "{agentName} أنشأ فريق جديد (#{id})",
"EDIT": "{agentName} قام بتحديث الفريق (#{id})",
"DELETE": "{agentName} حذف الفريق (#{id})"
},
"MACRO": {
"ADD": "{agentName} أنشأ ماكرو جديد (##{id})",
"EDIT": "{agentName} قام بتحديث ماكرو (##{id})",
"DELETE": "{agentName} حذف ماكرو (##{id})"
"ADD": "{agentName} أنشأ ماكرو جديد (#{id})",
"EDIT": "{agentName} قام بتحديث ماكرو (#{id})",
"DELETE": "{agentName} حذف ماكرو (#{id})"
},
"INBOX_MEMBER": {
"ADD": "{agentName} أضاف {user} إلى صندوق الوارد (##{inbox_id})",
"REMOVE": "{agentName} أزال {user} من صندوق الوارد (##{inbox_id})"
"ADD": "{agentName} أضاف {user} إلى صندوق الوارد (#{inbox_id})",
"REMOVE": "{agentName} أزال {user} من صندوق الوارد (#{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "{agentName} أضاف {user} إلى الفريق (##{team_id})",
"REMOVE": "{agentName} أزال {user} من الفريق (##{team_id})"
"ADD": "{agentName} أضاف {user} إلى الفريق (#{team_id})",
"REMOVE": "{agentName} أزال {user} من الفريق (#{team_id})"
},
"ACCOUNT": {
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (##{id})"
"EDIT": "{agentName} قام بتحديث إعدادات الحساب (#{id})"
},
"CONVERSATION": {
"DELETE": "{agentName} deleted conversation #{id}"
@@ -3,8 +3,11 @@
"HEADER": "الأتمتة",
"DESCRIPTION": "ويمكن للأتمتة أن تحل محل وتبسط العمليات القائمة التي تتطلب جهداً يدوياً، مثل إضافة تسميات وتعيين المحادثات إلى أنسب وكيل. ويسمح ذلك للفريق بالتركيز على مواطن قوتهم مع تقليل الوقت الذي يقضيه في المهام الروتينية.",
"LEARN_MORE": "تعلم المزيد عن الأتمتة",
"HEADER_BTN_TXT": "إضافة قاعدة أتمتة",
"COUNT": "{n} automation | {n} automations",
"HEADER_BTN_TXT": "Create Automation",
"LOADING": "جلب قواعد الأتمتة",
"SEARCH_PLACEHOLDER": "Search automation rules...",
"NO_RESULTS": "No automation rules found matching your search",
"ADD": {
"TITLE": "إضافة قاعدة أتمتة",
"SUBMIT": "إنشاء",
@@ -42,9 +45,9 @@
"LIST": {
"TABLE_HEADER": {
"NAME": "الاسم",
"DESCRIPTION": "الوصف",
"ACTIVE": "مفعل",
"CREATED_ON": "تم إنشاؤها في"
"CREATED_ON": "تم إنشاؤها في",
"ACTIONS": "الإجراءات"
},
"404": "لم يتم العثور على قواعد أتمتة"
},
@@ -150,7 +153,8 @@
"ADD_PRIVATE_NOTE": "Add a Private Note",
"CHANGE_PRIORITY": "تغيير الأولوية",
"ADD_SLA": "Add SLA",
"OPEN_CONVERSATION": "فتح المحادثة"
"OPEN_CONVERSATION": "فتح المحادثة",
"PENDING_CONVERSATION": "تحديد المحادثة كمعلقة"
},
"MESSAGE_TYPES": {
"INCOMING": "Incoming Message",
@@ -22,6 +22,10 @@
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
"UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى."
},
"RESOLVE": {
"ALL_MISSING_ATTRIBUTES": "لا يمكن حل المحادثات بسبب عدم وجود السمات المطلوبة",
"PARTIAL_SUCCESS": "بعض المحادثات تحتاج إلى سمات مطلوبة قبل الحل وتم تخطيها"
},
"LABELS": {
"ASSIGN_LABELS": "إضافة وسم",
"NO_LABELS_FOUND": "لم يتم العثور على تصنيفات",
@@ -3,8 +3,11 @@
"HEADER": "الردود الجاهزة",
"LEARN_MORE": "معرفة المزيد عن الاستجابات المعلبة",
"DESCRIPTION": "الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. يمكن للوكلاء كتابة حرف '/' يتبعه الرمز المختصر لإدراج رد مسبق أثناء محادثة. ",
"COUNT": "{n} canned response | {n} canned responses",
"HEADER_BTN_TXT": "إضافة رد جاهز",
"LOADING": "جاري جلب الردود الجاهزة...",
"SEARCH_PLACEHOLDER": "Search canned responses...",
"NO_RESULTS": "No canned responses found matching your search",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
"LIST": {
"404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.",
@@ -76,6 +76,9 @@
},
"waiting_since_desc": {
"TEXT": "الرد المعلق: الأقصر أولاً"
},
"priority_desc_created_at_asc": {
"TEXT": "Priority: Highest first, Created: Oldest first"
}
},
"ATTACHMENTS": {
@@ -104,7 +107,7 @@
"CONTENT": "Shared contact"
},
"embed": {
"CONTENT": "Embedded content"
"CONTENT": "المحتوى المضمن"
}
},
"CHAT_SORT_BY_FILTER": {
@@ -1,33 +1,33 @@
{
"COMPANIES": {
"HEADER": "Companies",
"HEADER": "الشركات",
"SORT_BY": {
"LABEL": "ترتيب حسب",
"OPTIONS": {
"NAME": "الاسم",
"DOMAIN": "النطاق",
"CREATED_AT": "تم إنشاؤها في",
"CONTACTS_COUNT": "Contacts count"
"CONTACTS_COUNT": "عدد جهات الاتصال"
}
},
"ORDER": {
"LABEL": "Order",
"LABEL": "ترتيب",
"OPTIONS": {
"ASCENDING": "Ascending",
"ASCENDING": "تصاعدي",
"DESCENDING": "Descending"
}
},
"SEARCH_PLACEHOLDER": "Search companies...",
"LOADING": "Loading companies...",
"UNNAMED": "Unnamed Company",
"CONTACTS_COUNT": "{n} contact | {n} contacts",
"SEARCH_PLACEHOLDER": "البحث في الشركات...",
"LOADING": "جاري تحميل الشركات...",
"UNNAMED": "شركة بلا اسم",
"CONTACTS_COUNT": "جهة اتصال {n} | {n} جهات الاتصال",
"EMPTY_STATE": {
"TITLE": "No companies found"
"TITLE": "لم يتم العثور على شركات"
}
},
"COMPANIES_LAYOUT": {
"PAGINATION_FOOTER": {
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
"SHOWING": "عرض {startItem} - {endItem} من {totalItems} شركة | عرض {startItem} – {endItem} من الشركات {totalItems}"
}
}
}
@@ -18,8 +18,8 @@
"CREATED_AT_LABEL": "تم إنشاؤها",
"NEW_MESSAGE": "رسالة جديدة",
"CALL": "Call",
"CALL_INITIATED": "Calling the contact…",
"CALL_FAILED": "Unable to start the call. Please try again.",
"CALL_INITIATED": "جار الاتصال بجهة الاتصال…",
"CALL_FAILED": "تعذر بدء المكالمة. الرجاء المحاولة مرة أخرى.",
"VOICE_INBOX_PICKER": {
"TITLE": "Choose a voice inbox"
},
@@ -457,8 +457,11 @@
"INSTAGRAM": {
"PLACEHOLDER": "Add Instagram"
},
"TELEGRAM": {
"PLACEHOLDER": "Add Telegram"
},
"TIKTOK": {
"PLACEHOLDER": "Add TikTok"
"PLACEHOLDER": "إضافة TikTok"
},
"LINKEDIN": {
"PLACEHOLDER": "Add LinkedIn"
@@ -573,7 +576,8 @@
"SEARCH_EMPTY_STATE_TITLE": "لا توجد جهات اتصال تطابق بحثك 🔍",
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋",
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
}
},
"LOAD_MORE": "تحميل المزيد"
},
"CONTACTS_BULK_ACTIONS": {
"ASSIGN_LABELS": "تعيين التسميات",
@@ -607,7 +611,7 @@
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
"CONTACT_SELECTOR": {
"LABEL": "إلى:",
"TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
"TAG_INPUT_PLACEHOLDER": "Enter at least 2 characters to search by name, email, or phone number",
"CONTACT_CREATING": "Creating contact..."
},
"INBOX_SELECTOR": {
@@ -618,9 +622,9 @@
"SUBJECT_LABEL": "الموضوع :",
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
"CC_LABEL": "نسخة من البريد:",
"CC_PLACEHOLDER": "Search for a contact with their email address",
"CC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_LABEL": "نسخة خفية من البريد:",
"BCC_PLACEHOLDER": "Search for a contact with their email address",
"BCC_PLACEHOLDER": "Enter at least 2 characters to search by email",
"BCC_BUTTON": "نسخة خفية من البريد"
},
"MESSAGE_EDITOR": {

Some files were not shown because too many files have changed in this diff Show More