Compare commits

...
Author SHA1 Message Date
Vishnu Narayanan cd016c5ad4 chore: log webhook response data on error 2023-11-24 14:18:13 +05:30
Sojan dcd18072b4 Merge branch 'release/3.3.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-11-18 10:24:25 -08:00
Sojan 4244945e7b Bump version to 3.3.1 2023-11-18 10:23:18 -08:00
Clairton Rodrigo HeinzenandGitHub 17725e4dd0 fix: Breakage of message pane when Openai integration isn't enabled (#8381)
Error when the Openai integration isn't enabled

fixes: https://github.com/chatwoot/chatwoot/issues/8379
2023-11-18 10:18:55 -08:00
Sojan d55bb63aa0 Merge branch 'release/3.3.0' into develop 2023-11-17 20:37:44 -08:00
Sojan bda2d1d9a4 Merge branch 'release/3.3.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-11-17 20:37:25 -08:00
Sojan 6e22695b85 Bump version to 3.3.0 2023-11-17 20:34:39 -08:00
Pranav Raj SandGitHub 146e46d79f fix: Disable AI Assist CTA if not required (#8375)
- Show CTAs only on production
2023-11-17 20:10:46 -08:00
Nithin David ThomasandGitHub 0af27a2387 feat: Adds support for logo in portal settings page [CW-2585] (#8354) 2023-11-17 19:58:27 -08:00
7380f0e7ce chore: Making OpenAI label suggestions optional (#8374)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-17 19:54:15 -08:00
Pranav Raj SandGitHub 2c5a0abcce fix: Update the height of the input based on the rows (#8373) 2023-11-17 17:29:44 -08:00
Sojan JoseandGitHub f23e33c1a9 perf: Remove unwanted conversation count query (#8372) 2023-11-17 16:40:40 -08:00
5588d6e344 feat: convert feature_flag to bigint [CW-2767] (#8350)
Co-authored-by: Sojan <sojan@pepalo.com>
2023-11-17 15:50:24 -08:00
59ace66c6f chore: [Snyk] Fix for vulnerabilities (#8340)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-17 15:24:00 -08:00
Sojan JoseandGitHub decef1ad52 chore: Add rails_panel gem for query debugging (#8370) 2023-11-18 02:30:35 +05:30
Sojan JoseandGitHub 0e19a4196b chore: Endpoint to purge portal logo (#8365) 2023-11-18 01:13:22 +05:30
Muhsin KelothandGitHub 04dd65687b feat: Changes to include delivery reports for all channels (#8329) 2023-11-16 19:43:35 +05:30
Muhsin KelothandGitHub 5f503b1a57 feat: Change slack bot and activity message appearance (#8349) 2023-11-16 12:59:52 +05:30
Sojan JoseandGitHub 5f6e974531 chore: Add website_token into request logs (#8362)
- Log website_token into request logs to identify specific widgets
2023-11-15 20:32:21 -08:00
Sojan JoseandGitHub a023eabf59 chore: Add logging for facebook errors (#8360)
- Add logging for Facebook errors
2023-11-15 14:56:21 -08:00
Sivin VargheseandGitHub cb1e25a487 feat: Change the highlight in article page on scrolling (#8330) 2023-11-15 13:51:46 +05:30
Sojan JoseandGitHub bcf7c6c45a chore: Improve account seeding (#8357) 2023-11-15 13:29:00 +05:30
Shivam MishraandGitHub b411b6ca21 feat: remove feature flag checks for message reply to (#8352) 2023-11-15 13:05:58 +05:30
Shivam MishraandGitHub cd217a3f86 feat: allow automation rules to remove labels (#8346) 2023-11-13 13:10:55 +05:30
Pranav Raj SandGitHub 6c79642450 chore: Update dashboard screenshots and the logo (#8342) 2023-11-11 23:08:05 +05:30
Nithin David ThomasandGitHub 201d476720 chore: Add meta viewport tag to public portal (#8335) 2023-11-09 20:30:58 -08:00
Muhsin KelothandGitHub c1a2e67c9e fix: Change the route change logic based on bus events, after creating a new conversation in the widget. (#8328) 2023-11-10 09:44:59 +05:30
1948bef9ac fix: Fix line break issue with link in portal header (#8323)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-11-08 20:38:38 -08:00
Sivin VargheseandGitHub 5486f39f7e feat: Update uncategorized block design (#8324) 2023-11-09 08:47:38 +05:30
f018b0013c feat: Updated the design of the article page (#8166)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:05:13 -08:00
950f085e80 feat: Updated the design of the category page (#8165)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:03:16 -08:00
268e26625b feat: Adds the ability to see the popular articles (#8152)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 18:01:07 -08:00
7041d86e4d feat: Updated home page design (#8106)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-11-08 17:59:03 -08:00
78398ee800 feat: Updated public portal header design (#8089)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-11-08 17:56:59 -08:00
Sojan JoseandGitHub 7b09b02737 chore: Fix trigger message for response Bot (#8322)
- Fix the bug where wrong messages was used to search response sources
2023-11-08 17:02:48 -08:00
Muhsin KelothandGitHub af7631d9f1 chore: Update MarkMessagesAsReadJob to accept delivered status (#8319) 2023-11-08 13:44:57 -08:00
Sivin VargheseandGitHub e0f33e62af feat: Adds support for image resize in the message bubble (#8182) 2023-11-08 14:11:47 +05:30
5d224f2e04 feat: allow inbox specific flags for signature toggle (#8280)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-11-08 10:47:22 +05:30
Gabriel QuinaandGitHub 4cb981d81b fix: Semantics across portuguese translations (#8291) 2023-11-08 10:42:57 +05:30
f891cfbf47 chore: [Snyk] Security upgrade sidekiq-cron from 1.10.1 to 1.11.0 (#8294)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-08 10:32:04 +05:30
Sojan JoseandGitHub e54c8a857d chore: Debug ChatGpt response (#8316)
- Add loggers to debug ChatGPT requests and responses
2023-11-07 20:07:29 -08:00
66229b0d6b chore: [Snyk] Security upgrade audited from 5.4.0 to 5.4.1 (#8305)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-11-07 17:45:32 -08:00
Muhsin KelothandGitHub 3eb1d49577 fix: Handle all types of errors in API channel webhooks (#8307) 2023-11-07 12:54:49 +05:30
Vishnu NarayananandGitHub 677888bcde feat: Add job to remove stale contact inboxes (#8096) 2023-11-06 20:24:30 -08:00
abbb4180ea chore: Add facebook business_management scope (#8004)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-11-06 18:37:41 -08:00
Pranav Raj SandGitHub d1cbc56d67 chore: Update widget config API to migrate the widget (#8303)
- Update the keys in the widget config API to support loading the widget separately from the Rails App.
2023-11-06 15:27:29 -08:00
EmanuelandGitHub b579ff832e chore: Update the case in the translation files (#8298) 2023-11-06 08:01:54 -08:00
Nithin David ThomasandGitHub 39d0748a5b feat: Lets users insert connected portal article into replies [CW-2282] (#8117)
- Lets users insert connected portal articles into replies

https://linear.app/chatwoot/issue/CW-2282/list-all-the-top-articles-from-the-connected-help-center
https://linear.app/chatwoot/issue/CW-1453/container-view-for-showing-search-input-and-result-items
2023-11-04 02:57:25 -07:00
Muhsin KelothandGitHub b4d20689b7 feat: Mark the messages as failed if the API channel webhooks fail for any reason. (#8277) 2023-11-04 12:26:28 +05:30
3b84b0fc47 feat: Creates 404 page for public portal [CW-2727] (#8293)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-11-04 04:59:44 +05:30
Nithin David ThomasandGitHub 23ea829510 chore: Refactors help center article url helper (#8269) 2023-11-04 04:18:34 +05:30
ebe9daea00 chore: Swagger documentation for contact labels APIs (#8279)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-11-03 15:09:12 +05:30
Shivam MishraandGitHub 36b6c0cb9c feat: support image height in markdown rendering of messages (#8177)
- This PR adds BaseMarkdownRenderer, it takes all the required attributes from the image node, parses the cw_image_height query and renders it.
2023-11-02 13:51:54 -07:00
Muhsin KelothandGitHub 16c36a78f0 chore: Remove sentry exceptions for the message status failed errors (#8285)
- Remove sending exceptions to Sentry after capturing the message failed errors.
2023-11-02 12:00:22 -07:00
Nithin David ThomasandGitHub 4999f7ed23 fix: Fixes wrong page size in category filter in articles page (#8270) 2023-11-02 15:19:19 +05:30
Chatwoot BotandGitHub e8f4ac632f chore: Update translations (#8255) 2023-11-01 15:13:32 -07:00
3c7854ccdf fix: Fixes error in ChatList component (#8266)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-11-02 03:23:29 +05:30
Pranav Raj SandGitHub 1e70223ed7 chore: Upgrade Cypress to 13.4.0 (#8271) 2023-10-31 19:39:34 -07:00
f34cd3ea61 chore: [Snyk] Security upgrade newrelic-sidekiq-metrics from 1.6.1 to 1.6.2 (#8254)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-10-31 18:24:18 -07:00
Muhsin KelothandGitHub 68b8d721a7 fix: Update the message status to failed if the outgoing Facebook message fails. (#8258) 2023-10-31 17:40:17 -07:00
Nithin David ThomasandGitHub d876419b27 fix: Update text area height while typing [CW-2438] (#8268) 2023-10-31 17:39:25 -07:00
8455186e9f feat: allow instagram reply_to [CW-2609] (#8248)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-31 17:31:12 -07:00
b6584ec68b feat: toggle reply to on widget based on feature flag (#8261)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-31 21:33:14 +05:30
Muhsin KelothandGitHub 29f18c7f18 fix: Handle Twilio::REST::RestError (#8257) 2023-10-31 16:23:34 +05:30
Muhsin KelothandGitHub 11b27f9805 feat: Handle Line send message/attachments errors (#8200) 2023-10-31 07:42:30 +05:30
Pranav Raj SandGitHub 653e0335c0 fix: Handle PermissionDeniedError for Dialogflow processor (#8252) 2023-10-30 13:24:03 -07:00
013dab7da6 fix: Add support for dark mode for the calendars (date and time picker). (#8250)
Co-authored-by: Liam Ashdown <liam.ashdown@debbiesvillas.co.uk>
2023-10-30 11:48:16 -07:00
Pranav Raj SandGitHub 2ba81830f3 fix: Update Instagram story rendering (#8240) 2023-10-30 11:16:14 -07:00
Sivin VargheseandGitHub dfba4770bd feat: Save sort Conversations filter (#8237) 2023-10-30 15:25:21 +05:30
LiamandGitHub 63e702ca12 fix: Fix an issue where Chat Widget Unread Message is not correctly d… (#8245) 2023-10-30 12:52:26 +05:30
Will KramerandGitHub 394fece846 docs: Update .env.example for clearer inbound Postmark instructions (#8235)
Updated the .env.example file to provide clearer instructions for setting up the Postmark inbound webhook. Many users, including myself, found the initial instructions ambiguous, leading to difficulties during setup. This change aims to simplify the process for future users by providing clearer instructions and examples.

Fixes: #8234
2023-10-27 16:06:27 -07:00
Muhsin KelothandGitHub 24fbab94c3 chore: Refactor MarkMessagesAsReadJob based on conversation id and time stamp (#8217)
- Mark all messages as read by providing the conversation ID and timestamp.
- For Instagram, ensure all previous messages that weren't marked as failed are now marked as read. This is because the read events are only triggered for the most recent message and not for any previous ones.
2023-10-27 15:21:39 -07:00
Muhsin KelothandGitHub 61e03fa33a feat: Bandwidth sms channel delivery reports (#8198)
Fixes: https://linear.app/chatwoot/issue/CW-2566/delivery-report-for-bandwidth-sms
2023-10-27 14:07:15 -07:00
Pranav Raj SandGitHub 6c4b92f7f6 fix: Revert the changes for SyncCustomFilterCountJob (#8238) 2023-10-27 10:45:48 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f023325b0e chore(deps): bump browserify-sign from 4.2.1 to 4.2.2 (#8233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-27 07:39:48 -07:00
dd05482cb0 chore: update UI copy (#8229)
Co-authored-by: Hricha Shandily <hrichashandily@gmail.com>
2023-10-27 16:05:42 +05:30
d94108bf3f feat: show ReplyTo in widget UI (#8094)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-27 13:35:02 +05:30
ab872beb1d feat: Helper to support dynamic system theme in public portal (#8206)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-26 12:59:51 +05:30
d02cfff4cc fix: canned responses not working when signature is present (#8176)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-26 11:35:46 +05:30
Robert CokerandGitHub 736f269c49 fix: Always reset CC/BCC emails when switching conversations (#8157) 2023-10-25 21:42:36 -07:00
Pranav Raj SandGitHub b067acfd4a fix: Update the alignment of the info icon on contact info panel (#8216) 2023-10-25 21:30:14 -07:00
Sivin VargheseandGitHub 232fa6fd18 fix: Revert missing files for search (#8215) 2023-10-26 09:08:28 +05:30
Chatwoot BotandGitHub 483d55df91 chore: Update translations (#8161) 2023-10-25 19:20:30 -07:00
3e54d3654b feat: Updated the search result fly-out menu design (#8203)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 19:16:49 -07:00
Sojan JoseandGitHub b6831d464e chore: Update API documentation for whatsapp templates (#8211)
- Update API documentation for whatsapp templates
2023-10-25 17:06:58 -07:00
Sojan JoseandGitHub 8c67ea56e7 chore: Campaign conversations should be created in open state (#8209)
- Ensure that conversations originating from campaigns are created in the open state.
2023-10-25 13:03:08 -07:00
Muhsin KelothandGitHub 54bc4c23df fix: Telegram model specs (#8207) 2023-10-26 00:14:09 +05:30
Muhsin KelothandGitHub 58f47eb02c feat: Handle Facebook send message/attachments errors (#8197) 2023-10-25 08:59:25 -07:00
4fc62ed9ae feat: Handle Instagram send message/attachments errors (#8174)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 08:56:09 -07:00
4a89bab23a feat: Handle Telegram send message/attachments errors (#8173)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-25 08:36:31 -07:00
Muhsin KelothandGitHub 7438f3b157 fix: Rescue slack ChannelNotFound error (#8196) 2023-10-25 13:53:30 +05:30
Shivam MishraandGitHub 9551dbc25f feat: add gtm to vueapp (#8154) 2023-10-24 15:51:21 +05:30
Sivin VargheseandGitHub 2e54f4a67c feat: Adds an event for the Google Translate usage (#8175) 2023-10-24 15:34:51 +05:30
Shivam MishraandGitHub 6d9b2a6b33 refactor: UI flags for Reply To (#8147) 2023-10-20 14:19:19 +05:30
Sivin VargheseandGitHub 35a9acf099 feat: Adds the ability to set an emoji for help center category (#8111) 2023-10-20 13:52:30 +05:30
b9694a0818 feat: support reply to for Telegram (#8105)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-20 13:14:20 +05:30
Shivam MishraandGitHub 7416bbb25e feat: support reply to for outgoing message in WhatsApp (#8107)
- This PR enables replies to WhatsApp.
2023-10-19 13:24:46 -07:00
Sojan 83231b9678 Merge branch 'release/3.2.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-10-17 17:55:34 -07:00
407 changed files with 6152 additions and 2541 deletions
+20 -1
View File
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to appropriate ingress channel with regards to incoming emails
# Set this to the appropriate ingress channel with regards to incoming emails
# Possible values are :
# relay for Exim, Postfix, Qmail
# mailgun for Mailgun
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage
ACTIVE_STORAGE_SERVICE=local
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx
# GOOGLE_TAG_MANAGER
# GOOGLE_TAG = GTM-XXXXXXX
## Scout
## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY
@@ -240,3 +253,9 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path
SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+1
View File
@@ -4,6 +4,7 @@ module.exports = {
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
'plugin:cypress/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
+7 -5
View File
@@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false
gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.12.0', require: false
gem 'sentry-rails', '>= 5.13.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.12.0', require: false
gem 'sentry-sidekiq', '>= 5.13.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.1.3'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.10.1'
gem 'sidekiq-cron', '>= 1.11.0'
##-- Push notification service --##
gem 'fcm'
@@ -159,7 +159,7 @@ gem 'lograge', '~> 0.14.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.0'
gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth
gem 'omniauth'
@@ -200,6 +200,8 @@ group :development do
# profiling
gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
end
group :test do
+33 -25
View File
@@ -126,9 +126,9 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.4.0)
activerecord (>= 5.0, < 7.2)
request_store (~> 1.2)
audited (5.4.1)
activerecord (>= 5.0, < 7.7)
activesupport (>= 5.0, < 7.7)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
aws-sdk-core (3.171.1)
@@ -148,6 +148,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19)
bindex (0.8.1)
blingfire (0.1.8)
@@ -174,7 +175,7 @@ GEM
rexml
crass (1.0.6)
csv-safe (3.2.1)
cypress-on-rails (1.13.1)
cypress-on-rails (1.16.0)
rack
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@@ -261,7 +262,7 @@ GEM
rake
flag_shih_tzu (0.3.23)
foreman (0.87.2)
fugit (1.8.1)
fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
gapic-common (0.18.0)
@@ -450,13 +451,16 @@ GEM
marcel (1.0.2)
maxminddb (0.1.22)
memoist (0.16.2)
meta_request (0.7.4)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.4)
mini_portile2 (2.8.5)
minitest (5.20.0)
mock_redis (0.36.0)
ruby2_keywords
@@ -478,10 +482,11 @@ GEM
net-smtp (0.3.3)
net-protocol
netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.1)
newrelic_rpm (~> 8)
newrelic-sidekiq-metrics (1.6.2)
newrelic_rpm (>= 8.0.0)
sidekiq
newrelic_rpm (8.16.0)
newrelic_rpm (9.6.0)
base64
nio4r (2.5.9)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
@@ -553,10 +558,12 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.1)
racc (1.7.3)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.1.1)
@@ -597,13 +604,13 @@ GEM
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rake (13.1.0)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.17.0)
redis-client (0.18.0)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -702,23 +709,23 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.12.0)
sentry-rails (5.13.0)
railties (>= 5.0)
sentry-ruby (~> 5.12.0)
sentry-ruby (5.12.0)
sentry-ruby (~> 5.13.0)
sentry-ruby (5.13.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.12.0)
sentry-ruby (~> 5.12.0)
sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.13.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.1.6)
sidekiq (7.2.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
sidekiq-cron (1.10.1)
sidekiq-cron (1.11.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
@@ -758,7 +765,7 @@ GEM
stripe (8.5.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.2.2)
thor (1.3.0)
tilt (2.2.0)
time_diff (0.3.0)
activesupport
@@ -838,7 +845,7 @@ DEPENDENCIES
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.4, >= 5.4.0)
audited (~> 5.4, >= 5.4.1)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -893,9 +900,10 @@ DEPENDENCIES
listen
lograge (~> 0.14.0)
maxminddb
meta_request
mock_redis
neighbor
newrelic-sidekiq-metrics
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm
omniauth
omniauth-google-oauth2
@@ -927,12 +935,12 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.12.0)
sentry-rails (>= 5.13.0)
sentry-ruby
sentry-sidekiq (>= 5.12.0)
sentry-sidekiq (>= 5.13.0)
shoulda-matchers
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1)
sidekiq-cron (>= 1.11.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
spring
+2 -2
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050
frontend: bin/webpack-dev-server
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
+10 -11
View File
@@ -1,10 +1,12 @@
<p align="center">
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
</p>
___
<p align="center">
# Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
@@ -13,9 +15,7 @@
</a>
</p>
___
<p align="center">
<p>
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
@@ -29,9 +29,8 @@ ___
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
@@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: message_type,
source_id: message_identifier,
content: message_content,
sender: @outgoing_echo ? nil : contact
sender: @outgoing_echo ? nil : contact,
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
}
end
+35 -3
View File
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
@automation_rule = content_attributes&.dig(:automation_rule_id)
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@in_reply_to = content_attributes&.dig(:in_reply_to)
@items = content_attributes&.dig(:items)
end
def perform
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments
return if @attachments.blank?
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count
@articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page).per(50)
@all_articles.order_by_position.page(@current_page)
else
@all_articles.order_by_updated_at.page(@current_page)
end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params
params.require(:category).permit(
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
)
end
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok
end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id)
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id],
message_type: :incoming
}
@@ -28,7 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc
conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
head :ok
end
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params
# timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
end
def set_message
@@ -23,7 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(@conversation)
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
head :ok
end
@@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
)
end
def portal
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params
params.permit(:query, :locale, :sort, :status)
end
@@ -14,10 +14,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
''
'system'
end
end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -40,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug])
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present?
article.category.locale
@@ -53,4 +59,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end
def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end
end
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
private
def set_category
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
end
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
render_404 && return if @category.blank?
end
end
+39 -2
View File
@@ -1,11 +1,48 @@
module PortalHelper
def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
end
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
def language_name(locale)
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
language_map[locale] || locale
end
def get_theme_names(theme)
if theme == 'light'
I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
I18n.t('public_portal.header.appearance.dark')
else
I18n.t('public_portal.header.appearance.system')
end
end
def get_theme_icon(theme)
if theme == 'light'
'icons/sun'
elsif theme == 'dark'
'icons/moon'
else
'icons/monitor'
end
end
def generate_gradient_to_bottom(theme)
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
end
def generate_article_link(portal_slug, article_slug, theme)
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
end
@@ -1,6 +1,7 @@
/* global axios */
import PortalsAPI from './portals';
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
class ArticlesAPI extends PortalsAPI {
constructor() {
@@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
portalSlug,
locale,
status,
author_id,
category_slug,
authorId,
categorySlug,
sort,
}) {
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
if (status !== undefined) baseUrl += `&status=${status}`;
if (author_id) baseUrl += `&author_id=${author_id}`;
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
return axios.get(baseUrl);
const url = getArticleSearchURL({
pageNumber,
portalSlug,
locale,
status,
authorId,
categorySlug,
sort,
host: this.url,
});
return axios.get(url);
}
searchArticles({ portalSlug, query }) {
const url = getArticleSearchURL({
portalSlug,
query,
host: this.url,
});
return axios.get(url);
}
getArticle({ id, portalSlug }) {
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`);
}
deleteLogo(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}/logo`);
}
}
export default PortalsAPI;
@@ -26,9 +26,13 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails);
if (toEmails) {
payload.append('to_emails', toEmails);
}
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
} else {
payload = {
content: message,
@@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
portalSlug: 'room-rental',
locale: 'en-US',
status: 'published',
author_id: '1',
authorId: '1',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
@@ -85,6 +85,33 @@ describe('#PortalAPI', () => {
window.axios = originalAxios;
});
it('#searchArticles', () => {
articlesAPI.searchArticles({
query: 'test',
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?query=test'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => {
articlesAPI.updateArticle({
articleId: 1,
@@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
contentAttributes: { in_reply_to: 12 },
files: [new Blob(['test-content'], { type: 'application/pdf' })],
});
expect(formPayload).toBeInstanceOf(FormData);
@@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual('');
expect(formPayload.get('bcc_emails')).toEqual('');
expect(formPayload.get('content_attributes')).toEqual(
'{"in_reply_to":12}'
);
});
it('builds object payload if file is not available', () => {
@@ -1,9 +1,5 @@
@import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
@apply z-[99999];
}
.date-picker {
&.no-margin {
.mx-input {
@@ -29,10 +25,52 @@
.mx-input[readonly] {
@apply bg-white dark:bg-slate-900 cursor-pointer;
}
.mx-icon-calendar {
@apply dark:text-slate-500;
}
}
.mx-calendar-content .cell:hover {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
.cell {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
&:hover,
&.hover-in-range,
&.in-range {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
}
.mx-time {
@apply border-0 bg-white dark:bg-slate-800;
.mx-time-header {
@apply border-0;
}
.mx-time-item {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-700;
}
}
}
.today {
@apply font-semibold;
}
}
.mx-datepicker-popup {
@apply z-[99999];
}
.mx-datepicker-inline {
@@ -41,24 +79,4 @@
.mx-calendar {
@apply w-full;
}
.cell.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
.mx-time-item.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
.today {
@apply font-semibold;
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
}
.mx-time-header {
@apply border-0;
}
}
@@ -16,3 +16,23 @@
width: 100%;
}
}
.integration-hooks {
.formulate-input[data-type='checkbox'] {
.formulate-input-wrapper {
@apply flex;
.formulate-input-element {
@apply pr-2;
input {
@apply mb-0;
}
}
}
.formulate-input-element-decorator {
@apply hidden;
}
}
}
@@ -207,6 +207,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -238,6 +239,7 @@ export default {
eventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
props: {
conversationInbox: {
@@ -408,8 +410,12 @@ export default {
},
conversationListPagination() {
const conversationsPerPage = 25;
const hasChatsOnView =
this.chatsOnView &&
Array.isArray(this.chatsOnView) &&
!this.chatsOnView.length;
const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage &&
@@ -515,6 +521,7 @@ export default {
},
},
mounted() {
this.setFiltersFromUISettings();
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData();
@@ -544,6 +551,12 @@ export default {
this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal();
},
setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
},
onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true;
},
@@ -136,6 +136,7 @@ export default {
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
dataTestid: 'sidebar-new-label-button',
children: this.labels.map(label => ({
id: label.id,
label: label.title,
@@ -78,6 +78,7 @@
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
@@ -61,6 +61,7 @@ export default {
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}),
isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed;
@@ -70,7 +71,8 @@ export default {
return (
this.isAdmin &&
!this.isAIIntegrationEnabled &&
!this.isAICTAModalDismissed
!this.isAICTAModalDismissed &&
this.isAChatwootInstance
);
},
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
@@ -135,6 +135,7 @@ export default {
// allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
},
data() {
@@ -266,8 +267,11 @@ export default {
sendWithSignature() {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled && this.allowSignature && !this.isPrivate;
if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
}
return false;
},
},
watch: {
@@ -442,7 +446,7 @@ export default {
this.onFocus();
},
click: () => {
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done.
this.isEditorMouseFocusedOnAnImage();
},
blur: () => {
this.onBlur();
@@ -670,7 +674,7 @@ export default {
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done.
this.updateImgToolbarOnDelete();
},
onKeydown(event) {
if (this.isEnterToSendEnabled()) {
@@ -101,7 +101,7 @@
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col"
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
>
<fluent-icon icon="cloud-backup" size="40" />
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
@@ -109,6 +109,16 @@
</h4>
</div>
</transition>
<woot-button
v-if="enableInsertArticleInReply"
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
icon="document-text-link"
color-scheme="secondary"
variant="smooth"
size="small"
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
@click="toggleInsertArticle"
/>
</div>
<div class="right-wrap">
<woot-button
@@ -233,6 +243,10 @@ export default {
type: Boolean,
default: false,
},
portalSlug: {
type: String,
required: true,
},
},
computed: {
...mapGetters({
@@ -299,14 +313,21 @@ export default {
return !this.isOnPrivateNote;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType);
},
signatureToggleTooltip() {
return this.sendWithSignature
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
},
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
},
},
mounted() {
ActiveStorage.start();
@@ -318,13 +339,14 @@ export default {
}
},
toggleMessageSignature() {
this.updateUISettings({
send_with_signature: !this.sendWithSignature,
});
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
},
replaceText(text) {
this.$emit('replace-text', text);
},
toggleInsertArticle() {
this.$emit('toggle-insert-article');
},
},
};
</script>
@@ -32,7 +32,7 @@
}}</span>
<filter-item
type="sort"
:selected-value="chatSortFilter"
:selected-value="sortFilter"
:items="chatSortItems"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter"
@@ -47,12 +47,13 @@ import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
components: {
FilterItem,
},
mixins: [clickaway],
mixins: [clickaway, uiSettingsMixin],
data() {
return {
showActionsDropdown: false,
@@ -83,8 +84,17 @@ export default {
closeDropdown() {
this.showActionsDropdown = false;
},
onChangeFilter(type, value) {
this.$emit('changeFilter', type, value);
onChangeFilter(value, type) {
this.$emit('changeFilter', value, type);
this.saveSelectedFilter(type, value);
},
saveSelectedFilter(type, value) {
this.updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : this.chatStatus,
order_by: type === 'sort' ? value : this.sortFilter,
},
});
},
},
};
@@ -22,22 +22,12 @@
:bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming"
/>
<blockquote v-if="storyReply" class="story-reply-quote">
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasImgStoryError && storyUrl"
:url="storyUrl"
@error="onStoryLoadError"
/>
<bubble-video
v-else-if="hasImgStoryError && storyUrl"
:url="storyUrl"
/>
</blockquote>
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
<bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo"
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
:message="inReplyTo"
:message-type="data.message_type"
:parent-has-attachments="hasAttachments"
/>
<bubble-text
v-if="data.content"
@@ -58,10 +48,15 @@
</span>
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in attachments" :key="attachment.id">
<instagram-story
v-if="isAnInstagramStory"
:story-url="attachment.data_url"
@error="onMediaLoadError"
/>
<bubble-image-audio-video
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment"
@error="onImageLoadError"
@error="onMediaLoadError"
/>
<bubble-location
v-else-if="attachment.file_type === 'location'"
@@ -74,9 +69,6 @@
:name="data.content"
:phone-number="attachment.fallback_title"
/>
<instagram-image-error-placeholder
v-else-if="hasImageError && hasInstagramStory"
/>
<bubble-file v-else :url="attachment.data_url" />
</div>
</div>
@@ -88,7 +80,6 @@
:story-id="`${storyId}`"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType"
:is-private="data.private"
:message-type="data.message_type"
@@ -137,19 +128,18 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue';
import BubbleImage from './bubble/Image.vue';
import BubbleVideo from './bubble/Video.vue';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleText from './bubble/Text.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue';
import BubbleText from './bubble/Text.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
@@ -162,19 +152,18 @@ import { LocalStorage } from 'shared/helpers/localStorage';
export default {
components: {
BubbleActions,
BubbleContact,
BubbleFile,
BubbleImage,
BubbleVideo,
BubbleImageAudioVideo,
BubbleIntegration,
BubbleLocation,
BubbleMailHead,
BubbleText,
BubbleContact,
BubbleReplyTo,
BubbleText,
ContextMenu,
InstagramStory,
InstagramStoryReply,
Spinner,
instagramImageErrorPlaceholder,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: {
@@ -190,17 +179,13 @@ export default {
type: Boolean,
default: false,
},
hasInstagramStory: {
type: Boolean,
default: false,
},
isWebWidgetInbox: {
type: Boolean,
default: false,
},
inboxSupportsReplyTo: {
type: Boolean,
default: false,
type: Object,
default: () => ({}),
},
inReplyTo: {
type: Object,
@@ -210,10 +195,9 @@ export default {
data() {
return {
showContextMenu: false,
hasImageError: false,
hasMediaLoadError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
hasImgStoryError: false,
};
},
computed: {
@@ -289,19 +273,22 @@ export default {
// since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to;
},
isAnInstagramStory() {
return this.contentAttributes.image_type === 'story_mention';
},
contextMenuEnabledOptions() {
return {
copy: this.hasText,
delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo,
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
};
},
contentAttributes() {
return this.data.content_attributes || {};
},
externalError() {
return this.contentAttributes.external_error || null;
return this.contentAttributes.external_error || '';
},
sender() {
return this.data.sender || {};
@@ -318,9 +305,6 @@ export default {
storyUrl() {
return this.contentAttributes.story_url || null;
},
storyReply() {
return this.storyUrl && this.hasInstagramStory;
},
contentType() {
const {
data: { content_type: contentType },
@@ -401,7 +385,7 @@ export default {
return false;
}
if (this.isFailed) {
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`);
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
}
return false;
},
@@ -459,13 +443,11 @@ export default {
},
watch: {
data() {
this.hasImageError = false;
this.hasImgStoryError = false;
this.hasMediaLoadError = false;
},
},
mounted() {
this.hasImageError = false;
this.hasImgStoryError = false;
this.hasMediaLoadError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
@@ -475,13 +457,13 @@ export default {
},
methods: {
isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video'].includes(fileType);
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasImageError;
return fileType === type && !this.hasMediaLoadError;
}
if (this.storyReply) {
return true;
@@ -494,11 +476,8 @@ export default {
async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data);
},
onImageLoadError() {
this.hasImageError = true;
},
onStoryLoadError() {
this.hasImgStoryError = true;
onMediaLoadError() {
this.hasMediaLoadError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu =
@@ -730,8 +709,4 @@ li.right {
}
}
}
.story-reply-quote {
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
}
</style>
@@ -31,7 +31,6 @@
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
@@ -54,7 +53,6 @@
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
@@ -116,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -238,10 +235,6 @@ export default {
isATweet() {
return this.conversationType === 'tweet';
},
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
isRightOrLeftIcon() {
if (this.isContactPanelOpen) {
return 'arrow-chevron-right';
@@ -291,13 +284,10 @@ export default {
return this.currentChat.unread_count || 0;
},
inboxSupportsReplyTo() {
return (
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
return {
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
};
},
},
@@ -350,7 +340,7 @@ export default {
// method available in mixin, need to ensure that integrations are present
await this.fetchIntegrationsIfRequired();
if (!this.isAIIntegrationEnabled) {
if (!this.isLabelSuggestionFeatureEnabled) {
return;
}
@@ -18,6 +18,12 @@
:popout-reply-box="popoutReplyBox"
@click="$emit('click')"
/>
<article-search-popover
v-if="showArticleSearchPopover && connectedPortalSlug"
:selected-portal-slug="connectedPortalSlug"
@insert="handleInsert"
@close="onSearchPopoverClose"
/>
<div class="reply-box__top">
<reply-to-message
v-if="shouldShowReplyToMessage"
@@ -35,7 +41,7 @@
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
:on-click="emojiOnClick"
:on-click="addIntoEditor"
/>
<reply-email-head
v-if="showReplyHead"
@@ -79,6 +85,7 @@
:variables="messageVariables"
:signature="signatureToApply"
:allow-signature="true"
:channel-type="channelType"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@@ -121,10 +128,12 @@
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
/>
<whatsapp-templates
:inbox-id="inbox.id"
@@ -154,6 +163,7 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
@@ -184,7 +194,6 @@ import {
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -206,6 +215,7 @@ export default {
Banner,
WhatsappTemplates,
MessageSignatureMissingAlert,
ArticleSearchPopover,
},
mixins: [
clickaway,
@@ -248,6 +258,7 @@ export default {
showCannedMenu: false,
showVariablesMenu: false,
newConversationModalActive: false,
showArticleSearchPopover: false,
};
},
computed: {
@@ -264,11 +275,7 @@ export default {
return (
this.inReplyTo?.id &&
!this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
);
},
showRichContentEditor() {
@@ -466,8 +473,7 @@ export default {
return !!this.signatureToApply;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
return this.fetchSignatureFlagFromUiSettings(this.channelType);
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
@@ -506,11 +512,18 @@ export default {
? this.messageSignature
: extractTextFromMarkdown(this.messageSignature);
},
connectedPortalSlug() {
const { help_center: portal = {} } = this.inbox;
const { slug = '' } = portal;
return slug;
},
},
watch: {
currentChat(conversation) {
const { can_reply: canReply } = conversation;
this.setCCAndToEmailsFromLastChat();
if (this.isOnPrivateNote) {
return;
}
@@ -521,7 +534,6 @@ export default {
this.replyType = REPLY_EDITOR_MODES.NOTE;
}
this.setCCAndToEmailsFromLastChat();
this.fetchAndSetReplyTo();
},
conversationIdByRoute(conversationId, oldConversationId) {
@@ -531,17 +543,25 @@ export default {
}
},
message(updatedMessage) {
this.hasSlashCommand =
updatedMessage[0] === '/' && !this.showRichContentEditor;
const hasNextWord = updatedMessage.includes(' ');
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
if (isShortCodeActive) {
this.mentionSearchKey = updatedMessage.substring(1);
this.showMentions = true;
} else {
this.mentionSearchKey = '';
this.showMentions = false;
}
// Check if the message starts with a slash.
const bodyWithoutSignature = removeSignature(
updatedMessage,
this.signatureToApply
);
const startsWithSlash = bodyWithoutSignature.startsWith('/');
// Determine if the user is potentially typing a slash command.
// This is true if the message starts with a slash and the rich content editor is not active.
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
this.showMentions = this.hasSlashCommand;
// If a slash command is active, extract the command text after the slash.
// If not, reset the mentionSearchKey.
this.mentionSearchKey = this.hasSlashCommand
? bodyWithoutSignature.substring(1)
: '';
// Autosave the current message draft.
this.doAutoSaveDraft();
},
replyType(updatedReplyType, oldReplyType) {
@@ -588,6 +608,23 @@ export default {
);
},
methods: {
handleInsert(article) {
const { url, title } = article;
if (this.isRichEditorEnabled) {
// Removing empty lines from the title
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
bus.$emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
} else {
this.addIntoEditor(
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
);
}
},
toggleRichContentEditor() {
this.updateUISettings({
display_rich_content_editor: !this.showRichContentEditor,
@@ -819,10 +856,18 @@ export default {
this.hideWhatsappTemplatesModal();
},
replaceText(message) {
if (this.sendWithSignature && !this.private) {
// if signature is enabled, append it to the message
// appendSignature ensures that the signature is not duplicated
// so we don't need to check if the signature is already present
message = appendSignature(message, this.signatureToApply);
}
const updatedMessage = replaceVariablesInMessage({
message,
variables: this.messageVariables,
});
setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage;
@@ -845,22 +890,22 @@ export default {
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertEmoji(emoji, selectionStart, selectionEnd) {
insertIntoTextEditor(text, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
emoji +
text +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
emojiOnClick(emoji) {
addIntoEditor(content) {
if (this.showRichContentEditor) {
this.updateEditorSelectionWith = emoji;
this.updateEditorSelectionWith = content;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertEmoji(emoji, selectionStart, selectionEnd);
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
}
},
clearMessage() {
@@ -964,6 +1009,19 @@ export default {
(item, index) => itemIndex !== index
);
},
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = [];
@@ -973,41 +1031,41 @@ export default {
const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId
: attachment.resource.file;
const attachmentPayload = {
let attachmentPayload = {
conversationId: this.currentChat.id,
files: [attachedFile],
private: false,
message: caption,
sender: this.sender,
};
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
multipleMessagePayload.push(attachmentPayload);
caption = '';
});
} else {
const messagePayload = {
let messagePayload = {
conversationId: this.currentChat.id,
message,
private: false,
sender: this.sender,
};
messagePayload = this.setReplyToInPayload(messagePayload);
multipleMessagePayload.push(messagePayload);
}
return multipleMessagePayload;
},
getMessagePayload(message) {
const messagePayload = {
let messagePayload = {
conversationId: this.currentChat.id,
message,
private: this.isPrivate,
sender: this.sender,
};
if (this.inReplyTo?.id) {
messagePayload.contentAttributes = {
in_reply_to: this.inReplyTo.id,
};
}
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = [];
@@ -1106,6 +1164,12 @@ export default {
// When new conversation modal is open
this.newConversationModalActive = isActive;
},
onSearchPopoverClose() {
this.showArticleSearchPopover = false;
},
toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover;
},
},
};
</script>
@@ -1124,7 +1188,7 @@ export default {
}
.reply-box {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
@apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private {
@apply bg-yellow-50 dark:bg-yellow-200;
@@ -114,10 +114,6 @@ export default {
type: Boolean,
default: true,
},
hasInstagramStory: {
type: Boolean,
default: true,
},
messageType: {
type: Number,
default: 1,
@@ -196,7 +192,7 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
// Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
@@ -204,24 +200,38 @@ export default {
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox
this.isAFacebookInbox ||
this.isASmsInbox ||
this.isATelegramChannel
) {
return this.sourceId && this.isSent;
}
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
return false;
},
showDeliveredIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isASmsInbox ||
this.isAFacebookInbox
) {
return this.sourceId && this.isDelivered;
}
// We will consider messages as delivered for web widget inbox and API inbox if they are sent
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent;
}
if (this.isALineChannel) {
return this.isDelivered;
}
return false;
},
@@ -229,12 +239,6 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWebWidgetInbox || this.isAPIInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
@@ -243,6 +247,10 @@ export default {
return this.sourceId && this.isRead;
}
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isRead;
}
return false;
},
},
@@ -1,6 +1,6 @@
<template>
<div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="onImgError()" />
<img :src="url" @click="onClick" @error="$emit('error')" />
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" />
</woot-modal>
@@ -28,9 +28,6 @@ export default {
onClick() {
this.show = true;
},
onImgError() {
this.$emit('error');
},
},
};
</script>
@@ -4,13 +4,14 @@
v-if="isImage && !isImageError"
:src="attachment.data_url"
@click="onClick"
@error="onImgError()"
@error="onImgError"
/>
<video
v-if="isVideo"
:src="attachment.data_url"
muted
playsInline
@error="onImgError"
@click="onClick"
/>
<audio v-else-if="isAudio" controls class="skip-context-menu">
@@ -21,7 +22,7 @@
:show.sync="show"
:attachment="attachment"
:all-attachments="filteredCurrentChatAttachments"
@error="onImgError()"
@error="onImgError"
@close="onClose"
/>
</div>
@@ -0,0 +1,51 @@
<template>
<bubble-image
v-if="!hasImgStoryError"
:url="storyUrl"
@error="onImageLoadError"
/>
<bubble-video
v-else-if="!hasVideoStoryError"
:url="storyUrl"
@error="onVideoLoadError"
/>
<instagram-story-error-place-holder v-else />
</template>
<script>
import BubbleImage from './Image.vue';
import BubbleVideo from './Video.vue';
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
export default {
components: {
BubbleImage,
BubbleVideo,
InstagramStoryErrorPlaceHolder,
},
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
this.emitError();
},
onVideoLoadError() {
this.hasVideoStoryError = true;
this.emitError();
},
emitError() {
this.$emit('error');
},
},
};
</script>
@@ -0,0 +1,14 @@
<template>
<div
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
>
<fluent-icon icon="document-error" size="32" />
<p class="mb-0 text-slate-700 dark:text-slate-75">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
</template>
<script>
export default {};
</script>
@@ -0,0 +1,35 @@
<template>
<blockquote
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
>
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
</blockquote>
</template>
<script>
import InstagramStory from './InstagramStory.vue';
export default {
components: { InstagramStory },
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
},
onVideoLoadError() {
this.hasVideoStoryError = true;
},
},
};
</script>
@@ -1,10 +1,11 @@
<template>
<div
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2"
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
:class="{
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
'-mx-2': !parentHasAttachments,
}"
>
<message-preview
@@ -33,6 +34,10 @@ export default {
type: Number,
required: true,
},
parentHasAttachments: {
type: Boolean,
required: true,
},
},
data() {
return { MESSAGE_TYPE };
@@ -1,12 +1,12 @@
<template>
<div class="video message-text__wrap">
<video :src="url" muted playsInline @click="onClick" />
<video ref="videoElement" :src="url" muted playsInline @click="onClick" />
<woot-modal :show.sync="show" :on-close="onClose">
<video
:src="url"
controls
playsInline
class="modal-video skip-context-menu"
class="modal-video skip-context-menu mx-auto"
/>
</woot-modal>
</div>
@@ -25,6 +25,11 @@ export default {
show: false,
};
},
mounted() {
this.$refs.videoElement.onerror = () => {
this.$emit('error');
};
},
methods: {
onClose() {
this.show = false;
@@ -1,26 +0,0 @@
<template>
<div class="image-placeholder">
<fluent-icon icon="document-error" size="32" />
<p>{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}</p>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.image-placeholder {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: var(--space-slab);
height: calc(var(--space-large) * 10);
margin-top: var(--space-one);
width: 100%;
background-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
}
</style>
+1 -1
View File
@@ -16,5 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
MESSAGE_REPLY_TO: 'message_reply_to',
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
};
@@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
SENT_MESSAGE: 'Sent a message',
SENT_PRIVATE_NOTE: 'Sent a private note',
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
TRANSLATE_A_MESSAGE: 'Translated a message',
INSERTED_A_VARIABLE: 'Inserted a variable',
USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations',
@@ -64,3 +64,47 @@ export const isValidURL = value => {
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
return URL_REGEX.test(value);
};
export const getArticleSearchURL = ({
host,
portalSlug,
pageNumber,
locale,
status,
authorId,
categorySlug,
sort,
query,
}) => {
const queryParams = new URLSearchParams({});
const params = {
page: pageNumber,
locale,
status,
author_id: authorId,
category_slug: categorySlug,
sort,
query,
};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
queryParams.set(key, value);
}
});
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
};
export const hasValidAvatarUrl = avatarUrl => {
try {
const { host: avatarUrlHost } = new URL(avatarUrl);
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
avatarUrlHost
);
return avatarUrl && !isFromGravatar;
} catch (error) {
return false;
}
};
@@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES,
};
return actionsMap[type];
@@ -3,6 +3,8 @@ import {
conversationUrl,
isValidURL,
conversationListPageURL,
getArticleSearchURL,
hasValidAvatarUrl,
} from '../URLHelper';
describe('#URL Helpers', () => {
@@ -75,4 +77,117 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false);
});
});
describe('getArticleSearchURL', () => {
it('should generate a basic URL without optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
it('should include status parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'published',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=published'
);
});
it('should include author_id parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
authorId: 123,
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&author_id=123'
);
});
it('should include category_slug parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
categorySlug: 'technology',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
);
});
it('should include sort parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
});
it('should handle multiple optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'draft',
authorId: 456,
categorySlug: 'science',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
);
});
it('should handle missing optional parameters gracefully', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
});
describe('hasValidAvatarUrl', () => {
test('should return true for valid non-Gravatar URL', () => {
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
});
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
false
);
});
test('should return false for a Gravatar URL (gravatar)', () => {
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
});
test('should handle invalid URL', () => {
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
});
test('should return false for empty or undefined URL', () => {
expect(hasValidAvatarUrl('')).toBe(false);
expect(hasValidAvatarUrl()).toBe(false);
});
});
});
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك"
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
"REMOVE": "حذف"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "الاسم",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "الاسم",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Име",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Име",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Suprimeix"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Odebrat"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Název",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Název",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Søg emojis",
"NOT_FOUND": "Ingen emoji matcher din søgning"
"NOT_FOUND": "Ingen emoji matcher din søgning",
"REMOVE": "Fjern"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Navn",
"PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Navn",
"PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Emojis durchsuchen",
"NOT_FOUND": "Kein Emoji entspricht deiner Suche"
"NOT_FOUND": "Kein Emoji entspricht deiner Suche",
"REMOVE": "Entfernen"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Αναζήτηση emojis",
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας"
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας",
"REMOVE": "Διαγραφή"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα"
},
"SLUG": {
@@ -1,18 +1,18 @@
{
"FILTER": {
"TITLE": "Filter Conversations",
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.",
"TITLE": "Filter conversations",
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
"EDIT_CUSTOM_FILTER": "Edit Folder",
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
"ADD_NEW_FILTER": "Add Filter",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
"ADD_NEW_FILTER": "Add filter",
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
"SUBMIT_BUTTON_LABEL": "Apply filters",
"UPDATE_BUTTON_LABEL": "Update folder",
"CANCEL_BUTTON_LABEL": "Cancel",
"CLEAR_BUTTON_LABEL": "Clear Filters",
"EMPTY_VALUE_ERROR": "Value is required",
"CLEAR_BUTTON_LABEL": "Clear filters",
"FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query",
"EMPTY_VALUE_ERROR": "Value is required.",
"TOOLTIP_LABEL": "Filter conversations",
"QUERY_DROPDOWN_LABELS": {
"AND": "AND",
@@ -36,44 +36,44 @@
},
"ATTRIBUTES": {
"STATUS": "Status",
"ASSIGNEE_NAME": "Assignee Name",
"INBOX_NAME": "Inbox Name",
"TEAM_NAME": "Team Name",
"CONVERSATION_IDENTIFIER": "Conversation Identifier",
"CAMPAIGN_NAME": "Campaign Name",
"ASSIGNEE_NAME": "Assignee name",
"INBOX_NAME": "Inbox name",
"TEAM_NAME": "Team name",
"CONVERSATION_IDENTIFIER": "Conversation identifier",
"CAMPAIGN_NAME": "Campaign name",
"LABELS": "Labels",
"BROWSER_LANGUAGE": "Browser Language",
"BROWSER_LANGUAGE": "Browser language",
"PRIORITY": "Priority",
"COUNTRY_NAME": "Country Name",
"COUNTRY_NAME": "Country name",
"REFERER_LINK": "Referer link",
"CUSTOM_ATTRIBUTE_LIST": "List",
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity"
"CREATED_AT": "Created at",
"LAST_ACTIVITY": "Last activity"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
"ADDITIONAL_FILTERS": "Additional Filters",
"CUSTOM_ATTRIBUTES": "Custom Attributes"
"STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional filters",
"CUSTOM_ATTRIBUTES": "Custom attributes"
},
"CUSTOM_VIEWS": {
"ADD": {
"TITLE": "Do you want to save this filter?",
"LABEL": "Name this filter",
"PLACEHOLDER": "Enter a name for this filter",
"ERROR_MESSAGE": "Name is required",
"PLACEHOLDER": "Name your filter to refer it later.",
"ERROR_MESSAGE": "Name is required.",
"SAVE_BUTTON": "Save filter",
"CANCEL_BUTTON": "Cancel",
"API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder created successfully",
"ERROR_MESSAGE": "Error while creating folder"
"SUCCESS_MESSAGE": "Folder created successfully.",
"ERROR_MESSAGE": "Error while creating folder."
},
"API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment created successfully",
"ERROR_MESSAGE": "Error while creating segment"
"SUCCESS_MESSAGE": "Segment created successfully.",
"ERROR_MESSAGE": "Error while creating segment."
}
},
"EDIT": {
@@ -83,19 +83,19 @@
"DELETE_BUTTON": "Delete filter",
"MODAL": {
"CONFIRM": {
"TITLE": "Confirm Deletion",
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete the filter ",
"YES": "Yes, Delete",
"NO": "No, Keep it"
"YES": "Yes, delete",
"NO": "No, keep it"
}
},
"API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder deleted successfully",
"ERROR_MESSAGE": "Error while deleting folder"
"SUCCESS_MESSAGE": "Folder deleted successfully.",
"ERROR_MESSAGE": "Error while deleting folder."
},
"API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment deleted successfully",
"ERROR_MESSAGE": "Error while deleting segment"
"SUCCESS_MESSAGE": "Segment deleted successfully.",
"ERROR_MESSAGE": "Error while deleting segment."
}
}
}
@@ -1,73 +1,73 @@
{
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"LOADING_EDITOR": "Loading editor...",
"HEADER_BTN_TXT": "Add bot configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>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.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
},
"SUBMIT": "Validate and save"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
"SUBMIT": "Update",
"DISCONNECT": "Disconnect Bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select Bot"
"DISCONNECT": "Disconnect bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
"SUCCESS_MESSAGE": "Bot added successfully.",
"ERROR_MESSAGE": "Could not add bot. Please try again later."
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete Bot",
"TITLE": "Delete bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
}
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
}
}
}
@@ -1,40 +1,40 @@
{
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent",
"AGENT_SELECT_LABEL": "Select agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign",
"YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_AGENT_TOOLTIP": "Assign agent",
"ASSIGN_TEAM_TOOLTIP": "Assign team",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully.",
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully.",
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"AGENT_LIST_LOADING": "Loading Agents",
"AGENT_LIST_LOADING": "Loading agents",
"UPDATE": {
"CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply.",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again"
"UPDATE_FAILED": "Failed to update conversations. Please try again."
},
"LABELS": {
"ASSIGN_LABELS": "Assign Labels",
"ASSIGN_LABELS": "Assign labels",
"NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again"
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
},
"TEAMS": {
"TEAM_SELECT_LABEL": "Select Team",
"TEAM_SELECT_LABEL": "Select team",
"NONE": "None",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
"ASSIGN_FAILED": "Failed to assign team. Please try again."
}
}
}
@@ -1,71 +1,71 @@
{
"CANNED_MGMT": {
"HEADER": "Canned Responses",
"HEADER_BTN_TXT": "Add Canned Response",
"LOADING": "Fetching Canned Responses",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
"HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching canned responses...",
"SEARCH_404": "There are no items matching this query.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": {
"404": "There are no canned responses available in this account.",
"TITLE": "Manage canned responses",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": ["Short Code", "Content", "Actions"]
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": ["Short code", "Content", "Actions"]
},
"ADD": {
"TITLE": "Add Canned Response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
"TITLE": "Add canned response",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Cancel",
"FORM": {
"SHORT_CODE": {
"LABEL": "Short Code",
"PLACEHOLDER": "Please enter a short code",
"ERROR": "Short Code is required"
"LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code.",
"ERROR": "Short Code is required."
},
"CONTENT": {
"LABEL": "Content",
"PLACEHOLDER": "Please enter a content",
"ERROR": "Content is required"
"LABEL": "Message",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Message is required."
},
"SUBMIT": "Submit"
},
"API": {
"SUCCESS_MESSAGE": "Canned Response added successfully",
"ERROR_MESSAGE": "Could not create canned response. Please try again later."
"SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}
},
"EDIT": {
"TITLE": "Edit Canned Response",
"TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Cancel",
"FORM": {
"SHORT_CODE": {
"LABEL": "Short Code",
"PLACEHOLDER": "Please enter a shortcode",
"ERROR": "Short Code is required"
"LABEL": "Short code",
"PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Short code is required."
},
"CONTENT": {
"LABEL": "Content",
"PLACEHOLDER": "Please enter a content",
"ERROR": "Content is required"
"LABEL": "Message",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Message is required."
},
"SUBMIT": "Submit"
},
"BUTTON_TEXT": "Edit",
"API": {
"SUCCESS_MESSAGE": "Canned Response updated successfully",
"ERROR_MESSAGE": "Could not update canned response. Please try again later."
"SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Canned response deleted successfully",
"ERROR_MESSAGE": "Could not delete canned response. Please try again later."
"SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
@@ -139,6 +139,7 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"INSERT_READ_MORE": "Read more",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Show rich text editor",
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}
@@ -76,6 +76,9 @@
},
"ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "Searching...",
"INSERT_ARTICLE": "Insert",
"NO_RESULT": "No articles found",
"COPY_LINK": "Copy article link to clipboard",
@@ -220,7 +223,10 @@
"LOGO": {
"LABEL": "Logo",
"UPLOAD_BUTTON": "Upload logo",
"HELP_TEXT": "This logo will be displayed on the portal header."
"HELP_TEXT": "This logo will be displayed on the portal header.",
"IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
"IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
"IMAGE_DELETE_ERROR": "Error while deleting logo"
},
"NAME": {
"LABEL": "Name",
@@ -365,7 +371,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +402,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -426,6 +432,21 @@
}
}
},
"ARTICLE_SEARCH": {
"TITLE": "Search articles",
"PLACEHOLDER": "Search articles",
"NO_RESULT": "No articles found",
"SEARCHING": "Searching...",
"SEARCH_BUTTON": "Search",
"INSERT_ARTICLE": "Insert link",
"IFRAME_ERROR": "URL is empty or invalid. Unable to display content.",
"OPEN_ARTICLE_SEARCH": "Insert article from Help Center",
"SUCCESS_ARTICLE_INSERTED": "Article inserted successfully",
"PREVIEW_LINK": "Preview article",
"CANCEL": "Close",
"BACK": "Back",
"BACK_RESULTS": "Back to results"
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
@@ -139,8 +139,8 @@
"PRIVATE_NOTE": "Nota privada",
"SEND": "Enviar",
"CREATE": "Añadir nota",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"DISMISS_REPLY": "Descartar respuesta",
"REPLYING_TO": "Respondiendo a:",
"TIP_FORMAT_ICON": "Mostrar editor de textos",
"TIP_EMOJI_ICON": "Mostrar selector de emoji",
"TIP_ATTACH_ICON": "Adjuntar archivos",
@@ -195,7 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copiar",
"REPLY_TO": "Reply to this message",
"REPLY_TO": "Responder a este mensaje",
"DELETE": "Eliminar",
"CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas",
"TRANSLATE": "Traducir",
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Buscar emojis",
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda"
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda",
"REMOVE": "Eliminar"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.",
"HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.",
"HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido"
},
"SLUG": {
@@ -428,28 +428,28 @@
},
"UPGRADE_PAGE": {
"TITLE": "Centro de ayuda",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Actualiza tu suscripción para activar esta característica.",
"SELF_HOSTED_DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Póngase en contacto con su administrador para habilitar esta característica.",
"BUTTON": {
"LEARN_MORE": "Más información",
"UPGRADE": "Upgrade"
"UPGRADE": "Mejorar"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
"TITLE": "Múltiples portales",
"DESCRIPTION": "Crear múltiples portales de centro de ayuda para diferentes productos usando la misma cuenta."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
"TITLE": "Soporte completo para locales",
"DESCRIPTION": "Localiza el portal en tu idioma. Apoyamos todas las localizaciones y permitimos traducciones para cada artículo."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
"TITLE": "Diseño amigable con SEO",
"DESCRIPTION": "Personalice sus meta-etiquetas para mejorar su visibilidad en los motores de búsqueda con nuestras páginas amigables con SEO."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
"TITLE": "Soporte completo de API",
"DESCRIPTION": "Utilice el portal como un CMS sin cabeceras con frameworks front-end de terceros usando nuestras APIs."
}
}
}
@@ -39,7 +39,7 @@
"SORT_TOOLTIP_LABEL": "مرتب‌سازی گفتگوها",
"CHAT_SORT": {
"STATUS": "وضعیت",
"ORDER_BY": "سفارش توسط"
"ORDER_BY": "مرتب بر اساس"
},
"CHAT_TIME_STAMP": {
"CREATED": {
@@ -62,7 +62,7 @@
"TEXT": "اولویت"
},
"sort_on_waiting_since": {
"TEXT": "پاسخ در انتظار"
"TEXT": "در انتظار پاسخ"
}
},
"ATTACHMENTS": {
@@ -9,7 +9,7 @@
"LOCATION": "مکان",
"BROWSER_LANGUAGE": "مرور زبان",
"CONVERSATION_TITLE": "جزئیات مکالمه",
"VIEW_PROFILE": "نمایش مشخصات",
"VIEW_PROFILE": "نمایش پروفایل",
"BROWSER": "مرورگر",
"OS": "سیستم عامل",
"INITIATED_FROM": "شروع شده از",
@@ -62,13 +62,13 @@
"DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید."
},
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "وارد كردن",
"TITLE": "وارد کردن مخاطبین",
"BUTTON_LABEL": "تزریق کردن",
"TITLE": "تزریق مخاطبین",
"DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.",
"DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.",
"FORM": {
"LABEL": "پرونده CSV",
"SUBMIT": "وارد كردن",
"SUBMIT": "تزریق کردن",
"CANCEL": "انصراف"
},
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
@@ -1,7 +1,7 @@
{
"CONVERSATION": {
"SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید",
"CSAT_REPLY_MESSAGE": "لطفاً به مکالمه امتیاز دهید",
"CSAT_REPLY_MESSAGE": "لطفاً به گفتگو امتیاز دهید",
"404": "با عرض پوزش، ما نمی‌توانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید",
"SWITCH_VIEW_LAYOUT": "تغییر طرح‌بندی",
"DASHBOARD_APP_TAB_MESSAGES": "پیام‌ها",
@@ -37,7 +37,7 @@
"REPLYING_TO": "شما در حال پاسخ دادن به:",
"REMOVE_SELECTION": "حذف انتخاب‌شده‌ها",
"DOWNLOAD": "دانلود",
"UNKNOWN_FILE_TYPE": "پرونده ناشناخته",
"UNKNOWN_FILE_TYPE": "فایل ناشناخته",
"SAVE_CONTACT": "ذخیره",
"UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوست‌ها...",
"REPLIED_TO_STORY": "Replied to your story",
@@ -46,7 +46,7 @@
"NO_RESPONSE": "بدون پاسخ",
"RATING_TITLE": "رتبه",
"FEEDBACK_TITLE": "بازخورد",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"REPLY_MESSAGE_NOT_FOUND": "پیام در دسترس نیست",
"CARD": {
"SHOW_LABELS": "مشاهده کردن برچسب‌ها",
"HIDE_LABELS": "پنهان کردن برچسب‌ها"
@@ -58,7 +58,7 @@
"OPEN": "بیشتر",
"CLOSE": "بستن",
"DETAILS": "جزئیات",
"SNOOZED_UNTIL": "Snoozed until",
"SNOOZED_UNTIL": "به تعویق افتاده تا",
"SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد"
@@ -76,23 +76,23 @@
"CUSTOM_SNOOZE": {
"TITLE": "به تعویق انداختن تا",
"APPLY": "به تعویق انداختن",
"CANCEL": "انصراف"
"CANCEL": "لغو"
},
"PRIORITY": {
"TITLE": "اولویت",
"OPTIONS": {
"NONE": "هیچکدام",
"URGENT": "Urgent",
"HIGH": "High",
"MEDIUM": "Medium",
"LOW": "Low"
"HIGH": "بالا",
"MEDIUM": "متوسط",
"LOW": "پایین"
},
"CHANGE_PRIORITY": {
"SELECT_PLACEHOLDER": "هیچکدام",
"INPUT_PLACEHOLDER": "Select priority",
"INPUT_PLACEHOLDER": "انتخاب اولویت",
"NO_RESULTS": "نتیجه‌ای یافت نشد",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
"FAILED": "Couldn't change priority. Please try again."
"SUCCESSFUL": "تغییر اولویت گفتگوی با شناسه %{conversationId} به %{priority}",
"FAILED": "تغییر اولویت گفتگو با موفقیت انجام نشد. لطفا دوباره تلاش نمایید."
}
},
"CARD_CONTEXT_MENU": {
@@ -139,8 +139,8 @@
"PRIVATE_NOTE": "یادداشت خصوصی",
"SEND": "ارسال",
"CREATE": "افزودن یادداشت",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"DISMISS_REPLY": "رد پاسخ",
"REPLYING_TO": "در حال پاسخ دهی به:",
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
"TIP_EMOJI_ICON": "انتخاب ایموجی",
"TIP_ATTACH_ICON": "ضمیمه فایل",
@@ -152,10 +152,10 @@
"STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا",
"": "",
"EMAIL_HEAD": {
"TO": "TO",
"ADD_BCC": "افزودن bcc",
"TO": "به",
"ADD_BCC": "افزودن رونوشت",
"CC": {
"LABEL": "CC",
"LABEL": "رونوشت",
"PLACEHOLDER": "ایمیل‌ها با کاما از هم جدا می‌شوند",
"ERROR": "لطفا آدرس‌ ایمیل معتبر وارد کنید"
},
@@ -195,7 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "کپی",
"REPLY_TO": "Reply to this message",
"REPLY_TO": "پاسخ به این پیام",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده",
"TRANSLATE": "ترجمه",
@@ -296,7 +296,7 @@
"SUBJECT": "موضوع"
},
"CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "شرکت کننده",
"SIDEBAR_MENU_TITLE": "شرکت کنندگان",
"SIDEBAR_TITLE": "شرکت کنندگان در گفتگو",
"NO_RECORDS_FOUND": "نتیجه‌ای یافت نشد",
"ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید",
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "جستجوی ایموجی",
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد"
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد",
"REMOVE": "حذف"
}
}
@@ -3,7 +3,7 @@
"TITLE": "تنظیمات حساب",
"SUBMIT": "به‌روزرسانی تنظیمات",
"BACK": "بازگشت",
"DISMISS": "Dismiss",
"DISMISS": "نادیده بگیر",
"UPDATE": {
"ERROR": "تنظیمات به‌روزرسانی نشد، دوباره امتحان کنید!",
"SUCCESS": "تنظیمات با موفقیت اعمال شد"
@@ -92,7 +92,7 @@
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "قطع کردن Chatwoot"
"TEXT": "ارتباط با سیستم چت قطع شده است"
},
"BUTTON": {
"REFRESH": "تازه کردن"
@@ -112,7 +112,7 @@
"REMOVE_LABEL": "برچسب را از مکالمه حذف کنید",
"SETTINGS": "تنظیمات",
"AI_ASSIST": "AI Assist",
"APPEARANCE": "Appearance"
"APPEARANCE": "ظاهری"
},
"COMMANDS": {
"GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید",
@@ -135,7 +135,7 @@
"ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید",
"ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید",
"AI_ASSIST": "AI Assist",
"ASSIGN_PRIORITY": "Assign priority",
"ASSIGN_PRIORITY": "انتساب اولویت",
"ASSIGN_A_TEAM": "یک تیم را تعیین کنید",
"MUTE_CONVERSATION": "صدای گفتگو را قطع کن",
"UNMUTE_CONVERSATION": "مکالمه را باصدا کنید",
@@ -147,8 +147,8 @@
"UNTIL_NEXT_REPLY": "تا پاسخ بعدی",
"UNTIL_NEXT_WEEK": "تا هفته بعد",
"UNTIL_TOMORROW": "تا فردا",
"UNTIL_NEXT_MONTH": "Until next month",
"AN_HOUR_FROM_NOW": "Until an hour from now",
"UNTIL_NEXT_MONTH": "تا ماه آینده",
"AN_HOUR_FROM_NOW": "از حالا تا یک ساعت دیگر",
"CUSTOM": "سفارشی...",
"CHANGE_APPEARANCE": "تغییر ظاهر",
"LIGHT_MODE": "روشن",
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است"
},
"SLUG": {
@@ -162,7 +162,7 @@
"PROVIDERS": {
"LABEL": "ارائه دهنده API",
"TWILIO": "Twilio",
"BANDWIDTH": "Bandwidth"
"BANDWIDTH": "پهنای باند"
},
"API": {
"ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم"
@@ -405,21 +405,21 @@
"DISABLED": "غیرفعال"
},
"SENDER_NAME_SECTION": {
"TITLE": "Sender name",
"TITLE": "نام ارسال کننده",
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
"FOR_EG": "For eg:",
"FRIENDLY": {
"TITLE": "Friendly",
"TITLE": "دوستانه",
"FROM": "از",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
},
"PROFESSIONAL": {
"TITLE": "Professional",
"TITLE": "حرفه ای",
"SUBTITLE": "Use only the configured business name as the sender name in the email header."
},
"BUSINESS_NAME": {
"BUTTON_TEXT": "+ Configure your business name",
"PLACEHOLDER": "Enter your business name",
"PLACEHOLDER": "نام کسب و کار خود را وارد کنید",
"SAVE_BUTTON_TEXT": "ذخیره"
}
},
@@ -486,7 +486,7 @@
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر",
"AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها",
"ENABLE_CSAT": "فعال کردن رضایت مشتری",
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
"SENDER_NAME_SECTION": "فعال سازی نام اپراتور در ایمیل",
"ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید",
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید",
@@ -498,7 +498,7 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.",
"HMAC_VERIFICATION": "اعتبار سنجی کاربر",
"HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، می‌توانید یک «شناسه_هش» برای هر کاربر ارسال کنید. می‌توانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.",
"HMAC_LINK_TO_DOCS": "You can read more here.",
"HMAC_LINK_TO_DOCS": "از اینجا می توانید بیشتر بخوانید.",
"HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر",
"HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواست‌هایی که «identifier_hash» را ندارند رد می‌شوند.",
"INBOX_IDENTIFIER": "شناسه صندوق ورودی",
@@ -674,7 +674,7 @@
"PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید."
},
"REPLY_TIME": {
"LABEL": "زمان پاسخ",
"LABEL": "زمان پاسخ دهی",
"IN_A_FEW_MINUTES": "در چند دقیقه",
"IN_A_FEW_HOURS": "در چند ساعت",
"IN_A_DAY": "در یک روز"
@@ -712,12 +712,12 @@
"CHAT": "گفتگو"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ می دهند",
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ دهی انجام می شود",
"IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند",
"IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند"
"IN_A_DAY": "به طور معمول در یک روز پاسخ دهی انجام می شود"
},
"FOOTER": {
"START_CONVERSATION_BUTTON_TEXT": "شروع گفتگو",
"START_CONVERSATION_BUTTON_TEXT": "گفتگو را شروع کنید",
"CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید"
},
"BODY": {
@@ -131,7 +131,7 @@
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
"BUTTONS": {
"NEED_HELP": "به کمک احتياج داری؟",
"DISMISS": "Dismiss",
"DISMISS": "نادیده بگیر",
"FINISH": "Finish Setup"
},
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
@@ -46,7 +46,7 @@
"DISMISS": "Dismiss suggestion"
},
"POWERED_BY": "Chatwoot AI",
"DISMISS": "Dismiss",
"DISMISS": "نادیده بگیر",
"ADD_SELECTED_LABELS": "Add selected labels",
"ADD_SELECTED_LABEL": "Add selected label",
"ADD_ALL_LABELS": "Add all labels"
@@ -34,7 +34,7 @@
"ERROR": "Inbox is required"
},
"MESSAGE": {
"LABEL": "Message",
"LABEL": "Viesti",
"PLACEHOLDER": "Please enter the message of campaign",
"ERROR": "Message is required"
},
@@ -88,7 +88,7 @@
"404": "There are no campaigns created for this inbox.",
"TABLE_HEADER": {
"TITLE": "Title",
"MESSAGE": "Message",
"MESSAGE": "Viesti",
"INBOX": "Inbox",
"STATUS": "Tila",
"SENDER": "Sender",
@@ -1,75 +1,75 @@
{
"CANNED_MGMT": {
"HEADER": "Tallennetut vastaukset",
"HEADER_BTN_TXT": "Lisää tallennettu vastaus",
"LOADING": "Haetaan tallennettuja vastauksia",
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>",
"HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching canned responses...",
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": {
"404": "Tällä tilillä ei ole tallennettuja vastauksia.",
"TITLE": "Hallitse tallennettuja vastauksia",
"DESC": "Tallennetut vastaukset ovat ennalta valmisteltuja vastausmalleja, joita voidaan käyttää nopeasti lähettää vastaukset tukipyyntöihin.",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": [
"Oikotie",
"Short code",
"Sisältö",
"Toiminnot"
]
},
"ADD": {
"TITLE": "Lisää tallennettu vastaus",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
"TITLE": "Add canned response",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": {
"SHORT_CODE": {
"LABEL": "Oikotie",
"PLACEHOLDER": "Please enter a short code",
"ERROR": "Oikotie on vaadittu"
"LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code.",
"ERROR": "Short Code is required."
},
"CONTENT": {
"LABEL": "Sisältö",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö",
"ERROR": "Sisältö on vaadittu"
"LABEL": "Viesti",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Message is required."
},
"SUBMIT": "Lähetä"
},
"API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus lisätty onnistuneesti",
"SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
}
},
"EDIT": {
"TITLE": "Muokkaa tallennettua vastausta",
"TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": {
"SHORT_CODE": {
"LABEL": "Oikotie",
"PLACEHOLDER": "Ole hyvä ja syötä oikotie",
"ERROR": "Oikotie on vaadittu"
"LABEL": "Short code",
"PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Short code is required."
},
"CONTENT": {
"LABEL": "Sisältö",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö",
"ERROR": "Sisältö on vaadittu"
"LABEL": "Viesti",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Message is required."
},
"SUBMIT": "Lähetä"
},
"BUTTON_TEXT": "Muokkaa",
"API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus päivitetty onnistuneesti",
"SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
}
},
"DELETE": {
"BUTTON_TEXT": "Poista",
"API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus poistettu onnistuneesti",
"SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
},
"CONFIRM": {
"TITLE": "Vahvista poistaminen",
"TITLE": "Confirm deletion",
"MESSAGE": "Oletko varma että haluat poistaa ",
"YES": "Kyllä, poista ",
"NO": "Ei, säilytä "
"YES": "Yes, delete ",
"NO": "No, keep "
}
}
}
@@ -200,7 +200,7 @@
"ERROR": "Subject can't be empty"
},
"MESSAGE": {
"LABEL": "Message",
"LABEL": "Viesti",
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
@@ -309,20 +309,20 @@
},
"VALUE": {
"LABEL": "Attribute value",
"PLACEHOLDER": "Eg: 11901 "
"PLACEHOLDER": "Esim: 11901 "
},
"ADD": {
"TITLE": "Luo uusi määrite ",
"SUCCESS": "Attribute added successfully",
"ERROR": "Unable to add attribute. Please try again later"
"SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
},
"UPDATE": {
"SUCCESS": "Attribute updated successfully",
"ERROR": "Unable to update attribute. Please try again later"
"SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
},
"DELETE": {
"SUCCESS": "Attribute deleted successfully",
"ERROR": "Unable to delete attribute. Please try again later"
"SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
},
"ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä",
@@ -331,15 +331,15 @@
},
"ATTRIBUTE_TYPE": {
"LIST": {
"PLACEHOLDER": "Select value",
"SEARCH_INPUT_PLACEHOLDER": "Search value",
"PLACEHOLDER": "Valitse arvo",
"SEARCH_INPUT_PLACEHOLDER": "Etsi arvo",
"NO_RESULT": "Tuloksia ei löytynyt"
}
}
},
"VALIDATIONS": {
"REQUIRED": "Valid value is required",
"INVALID_URL": "Invalid URL"
"REQUIRED": "Kelvollinen arvo vaaditaan",
"INVALID_URL": "Virheellinen URL"
}
},
"MERGE_CONTACTS": {
@@ -355,7 +355,7 @@
"HELP_LABEL": "Säilytetään"
},
"SUMMARY": {
"TITLE": "Summary",
"TITLE": "Yhteenveto",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
@@ -270,17 +270,17 @@
"CONVERSATION_CUSTOM_ATTRIBUTES": {
"ADD_BUTTON_TEXT": "Create attribute",
"UPDATE": {
"SUCCESS": "Attribute updated successfully",
"ERROR": "Unable to update attribute. Please try again later"
"SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
},
"ADD": {
"TITLE": "Add",
"SUCCESS": "Attribute added successfully",
"ERROR": "Unable to add attribute. Please try again later"
"SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
},
"DELETE": {
"SUCCESS": "Attribute deleted successfully",
"ERROR": "Unable to delete attribute. Please try again later"
"SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
},
"ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä",
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Poista"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Nimi",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Nimi",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -410,7 +410,7 @@
"FOR_EG": "For eg:",
"FRIENDLY": {
"TITLE": "Friendly",
"FROM": "from",
"FROM": "lähettäjä",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
},
"PROFESSIONAL": {
@@ -19,7 +19,7 @@
"BOT_LABEL": "Botti",
"READ_MORE": "Read more",
"WROTE": "wrote:",
"FROM": "from",
"FROM": "lähettäjä",
"EMAIL": "sähköposti"
}
}
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Rechercher des émojis",
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche"
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche",
"REMOVE": "Supprimer"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "חפש אימוג'י",
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך"
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך",
"REMOVE": "הסר"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -1,6 +1,7 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Izbaci"
}
}

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