Compare commits

...
93 Commits
Author SHA1 Message Date
Vishnu Narayanan 4ffe73a1ce Merge branch 'hotfix/2.13.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-02-03 18:47:31 +05:30
Vishnu Narayanan 76b0279571 chore: Bump version to v2.13.1 2023-02-03 18:46:25 +05:30
Vishnu Narayanan 76731ae31a chore: fix docker public assets issue #6341 2023-02-03 18:45:35 +05:30
Sojan e054c2a32f Merge branch 'release/2.13.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2023-01-17 18:20:45 +05:30
Sojan dbe27e081e Bump version to 2.13.0 2023-01-17 18:19:12 +05:30
Pranav Raj SandGitHub f6a56edf86 feat: Add the UI flow for Microsoft Oauth (#6243) 2023-01-17 17:48:14 +05:30
Jordan BroughandGitHub 66cb0ee865 chore: Refactor code in ApplicationMailbox (#5857)
Refactor code in ApplicationMailbox

* short-circuiting as soon as we get a "true" value in some cases
* using ".exists?" instead of instantiating an ActiveRecord object
* using "match?" instead of "match"
2023-01-17 17:30:03 +05:30
Prithvi TharunandGitHub 74b9c79f49 Fixes #3670 (#6265)
Success toast content for copy action made generic that fits all the context and scenarios.
2023-01-17 15:45:59 +05:30
Jan MatuszewskiandGitHub d46f96e45c Fix performance of report builder spec (#6024) 2023-01-17 09:27:50 +05:30
Chatwoot BotandGitHub 032699930c chore: Update translations from Crowdin (#6240) 2023-01-16 16:07:00 -08:00
Pranav Raj SandGitHub 16085fdfbd fix: Add missing translations (#6263) 2023-01-16 15:43:18 -08:00
00cbdaa8ca Feat: Support for Microsoft Oauth in Email Channel (#6227)
- Adds the backend APIs required for Microsoft Email Channels

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Sojan <sojan@pepalo.com>
2023-01-17 02:39:05 +05:30
Pranav Raj SandGitHub d0972a22b4 chore: Update styles for canned response (#6262) 2023-01-16 12:40:13 -08:00
e707778490 feat: Revamp editor for message and article (#6145)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-01-16 09:08:47 -08:00
0d894e0abc fix: Add animation to live chat trigger button (#6252)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-01-16 09:07:10 -08:00
Sojan JoseandGitHub 72f206025a chore: Fix connection pool (#6005)
We want to allocate as much database connection to the pool to match the sidekiq concurrency configuration.
ref: https://maxencemalbois.medium.com/the-ruby-on-rails-database-connections-pool-4ce1099a9e9f

fixes: #6004
2023-01-16 17:50:23 +05:30
70cb0a8ed9 fix: Use account locale as the default locale on widget (#6248)
* fix: Use account locale as the default locale on widget

* Refactors check for invalid locale

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-01-16 12:14:35 +05:30
d488a42664 feat: Allow disconnecting agent bots (#6245)
* Allow disconnecting the bot

* Code Climate fix

* Show error message if exists

* Codeclimate test - rename file

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-01-13 22:28:45 +05:30
ooooooo_qandGitHub 9e4a5d028c fix: Path traversal at SwaggerController (#6205) 2023-01-12 23:44:21 +05:30
Pranav Raj SandGitHub 9bbadc2388 chore: Create a resuable component for selecting a channel (#6241) 2023-01-11 21:13:51 -08:00
Sojan JoseandGitHub ad782e450e fix: Stop processing broken liquid tags (#6236) 2023-01-11 17:41:02 +05:30
Fayaz AhmedandGitHub 98eb20d600 Use the table-layout: fixed prop (#6232) 2023-01-11 11:37:30 +05:30
0ba5e47308 chore: Update the widescreen layout button icon and background (#6217)
* Update the button icon and background

* Use the woot-button

* Minor css cleanup

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
2023-01-11 00:40:48 +05:30
d041f5fc9f fix: Fixes inconsistent spacing in message bubble (#6219)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-01-10 20:51:21 +05:30
Sivin VargheseandGitHub 14382ad881 fix: Not able to create a portal if the custom domain is empty (#6224) 2023-01-10 20:50:35 +05:30
Sojan JoseandGitHub c16624dc5d fix: Duplicate messages in Whatsapp Channel (#6220)
fixes: #5433
2023-01-10 18:57:34 +05:30
Fayaz AhmedandGitHub 60f16e8954 Use woot-editor instead of native textarea (#6216) 2023-01-10 17:50:16 +05:30
078ff615ee feat: Add support for template variables in messages content (#6215)
Fixes: #6078

Co-authored-by: Sojan <sojan@pepalo.com>
2023-01-10 16:00:34 +05:30
Sivin VargheseandGitHub e7a52c3a46 fix: If desktop view is false then the view not getting updated (#6214)
* fix: Desktop view is false then the view not getting updated

* chore: Handle sidebar in small screen

* fix: Scroll issue in touch screens
2023-01-10 15:36:16 +05:30
add33d032c feat: Adds automation view for assigning an agent (#6131)
* feat: Adds automation view for assigning an agent

Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-01-10 11:35:56 +05:30
Sojan JoseandGitHub 28c8023bad chore: Update translations (#6211) 2023-01-09 21:23:11 -08:00
817ec4c50f fix: Fix the default article sidebar state (#6191)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-01-09 20:16:53 -08:00
ffb4bd0109 feat: Add the support for video calls with Dyte in the live-chat widget (#6208)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2023-01-09 11:52:31 -08:00
24cf7af30b feat: Add video call option with Dyte in the dashboard (#6207)
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-01-09 11:49:27 -08:00
0a65a233d7 fix: Invalid portal domain validation (#6166)
* fix: Invalid portal domain validation


Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-01-09 13:42:19 +05:30
4172cb4b23 feat: Uses expanded layout as default if the breakpoint is less than or equal to 992px (#6134)
* feat: Uses expanded layout as default if the breakpoint is below 992px

* fixes: ES lint issue

* chore: Review fixes

* chore: Hide toggle sidebar menu in profile settings

* chore: Fix login screen

* Update app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>

* chore: Review fixes

* chore: Review fixes

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-01-09 13:26:31 +05:30
Pranav Raj SandGitHub cbfbe6dbad feat: Add backend APIs for Dyte integration (#6197)
- The backend changes required for Dyte Integration.
2023-01-09 12:37:18 +05:30
Shivam MishraandGitHub 50894fd591 fix: Add padding for an empty state (#6190) 2023-01-06 13:44:30 -08:00
Sojan JoseandGitHub fbdc921ee5 chore: Fix sentry errors for Instagram channel (#6189)
fixes: #6188
2023-01-06 17:18:31 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Pranav Raj S
3beafe31f8 chore(deps): bump express from 4.17.1 to 4.18.2 (#6030)
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-01-06 16:01:43 +05:30
74c6db37b3 fix: Fixes broken style in automation page (#6171)
* fix: Fixes broken style in automation page

* Fix the position of drag handle

Co-authored-by: fayazara <fayazara@gmail.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
2023-01-06 15:37:28 +05:30
Hricha ShandilyandGitHub 4bbd56f87d chore: Update error message in Custom Attributes (#6187) 2023-01-06 13:58:05 +05:30
Shivam MishraandGitHub b9ae664d47 fix: Update incorrect label for team selection (#6180) 2023-01-05 19:35:11 -08:00
Sojan JoseandGitHub 269ad8f697 Fix: Errors in Heroku deployments (#6182)
Due to 86ca7f4a8d the redis configuration in Heroku deployments was breaking, temporarily reverting the part until we identify a fix.

fixes: #5938
2023-01-05 18:36:21 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9cb75b55f7 chore(deps): bump json5 from 1.0.1 to 1.0.2 (#6168)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-05 14:30:41 +05:30
Pranav Raj SandGitHub fc06ee7726 fix: Update the page size to 15, change default sort to last_activity (#6177) 2023-01-04 15:39:03 -08:00
Sojan JoseandGitHub 88053c60c7 chore: Update translations (#6160) 2023-01-04 11:03:02 -08:00
OMAR.AandGitHub 7b5f1e4876 feat: Support for telegram incoming location message (#6158)
Support for incoming location messages in the telegram channel.

ref: #3398
2023-01-04 16:11:54 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c88ea257d5 chore(deps): bump httparty from 0.20.0 to 0.21.0 (#6164)
Bumps [httparty](https://github.com/jnunemaker/httparty) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/jnunemaker/httparty/releases)
- [Changelog](https://github.com/jnunemaker/httparty/blob/master/Changelog.md)
- [Commits](https://github.com/jnunemaker/httparty/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: httparty
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 14:35:57 +05:30
Pranav Raj SandGitHub dd0d221cfc 2022 -> 2023 2023-01-01 22:12:22 -08:00
Sojan JoseandGitHub ab87f9d6c0 chore: Update translations (#6159) 2023-01-01 21:40:15 -08:00
e3f13dd455 choreButtons must have discernible text (#6132)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-01-01 21:39:38 -08:00
Pranav Raj S 4941c93cb0 Merge branch 'develop'
Publish Chatwoot CE docker images / build (push) Waiting to run
2022-12-30 13:58:18 -08:00
Pranav Raj S c11eebb83a Bump version to v2.12.1 2022-12-30 13:54:37 -08:00
Pranav Raj S 69bed3e08d fix: Update unread_messages count in WhatsApp inboxes (#6154)
* fix: Update unread_count from backend instead of computing on the frontend

* Fix spec

* Remove status indicator on private notifications

* CodeClimate fix

* CodeClimate
2022-12-30 13:54:37 -08:00
Sojan JoseandPranav Raj S cf48610f2f chore: Update translations (#6152) 2022-12-30 13:54:37 -08:00
Nithin David ThomasandPranav Raj S b179d1b563 chore: Refactor shortcuts modal to use modal component (#6151)
* chore: Refactor SHortcuts modal to use modal component

* Typography changes
2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S 34e509b9b7 feat: Adds the ability to play audio alert every 30 sec (#6150)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-30 13:54:37 -08:00
Pranav Raj S 6ffb7fe34e chore: Refactor audio notification helper (#6148) 2022-12-30 13:54:37 -08:00
Pranav Raj S 297e144f33 fix: Update account limit selection logic (#6149) 2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S 02a687b226 feat: Add new audio alert options (#6141)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-30 13:54:37 -08:00
Sojan JoseandPranav Raj S 10a03cae24 chore: Update translations (#6129) 2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S e446a12ebb chore: Fixes issue showing the CSAT error message (#6136)
Approved by Muhsin
2022-12-30 13:54:37 -08:00
Nithin David ThomasandPranav Raj S 00eabe07a4 fix: Expand title height of textarea on load (#6103) 2022-12-30 13:54:37 -08:00
Sojan JoseandPranav Raj S 78251f54c9 chore: Update translations (#6113) 2022-12-30 13:54:37 -08:00
d55988a6e0 feat: Add the ability to toggle the secondary sidebar in all display breakpoints (#6118)
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-30 13:54:37 -08:00
Muhsin KelothandPranav Raj S 37aa602c61 chore: Increase the max concurrent number of devices (#6121) 2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S aba5a1a131 chore: Helpcenter improvements (#6098) 2022-12-30 13:54:37 -08:00
94ca2fd229 feat: Send audio longer than 10 seconds and Add Prop audio-record-format (#6108)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-12-30 13:54:37 -08:00
Pranav Raj S a3e5aad753 fix: Update the logic to show read status for web (#6107) 2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S 8e13a06833 fix: Use canned response menu from the editor in contact messages (#6109) 2022-12-30 13:54:37 -08:00
Sivin VargheseandPranav Raj S 38f9c3940b chore: Hide inbox name only has one inbox (#6115) 2022-12-30 13:54:37 -08:00
Pranav Raj S e026dc03e0 Merge branch 'release/2.12.1' 2022-12-30 13:49:57 -08:00
Pranav Raj S d911d38391 Bump version to v2.12.1 2022-12-30 13:49:09 -08:00
Pranav Raj SandGitHub 5d1be70e67 fix: Update unread_messages count in WhatsApp inboxes (#6154)
* fix: Update unread_count from backend instead of computing on the frontend

* Fix spec

* Remove status indicator on private notifications

* CodeClimate fix

* CodeClimate
2022-12-30 13:13:33 -08:00
Sojan JoseandGitHub 12b45f5c76 chore: Update translations (#6152) 2022-12-30 09:38:28 -08:00
Nithin David ThomasandGitHub 9f842bcd59 chore: Refactor shortcuts modal to use modal component (#6151)
* chore: Refactor SHortcuts modal to use modal component

* Typography changes
2022-12-30 22:26:16 +05:30
4fba9ef1fb feat: Adds the ability to play audio alert every 30 sec (#6150)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-29 22:16:58 -08:00
Pranav Raj SandGitHub cd3b6ebf28 chore: Refactor audio notification helper (#6148) 2022-12-29 20:01:14 -08:00
Pranav Raj SandGitHub 11f6e6bc61 fix: Update account limit selection logic (#6149) 2022-12-29 19:37:36 -08:00
e5fd866a7c feat: Add new audio alert options (#6141)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-29 10:21:23 -08:00
Sojan JoseandGitHub e5219df2b4 chore: Update translations (#6129) 2022-12-28 13:56:49 -08:00
Sivin VargheseandGitHub 98c289dc3e chore: Fixes issue showing the CSAT error message (#6136)
Approved by Muhsin
2022-12-28 12:49:11 +05:30
Nithin David ThomasandGitHub 3e91765472 fix: Expand title height of textarea on load (#6103) 2022-12-22 14:14:30 -08:00
Sojan JoseandGitHub 1bf23055df chore: Update translations (#6113) 2022-12-22 14:08:08 -08:00
2af337be10 feat: Add the ability to toggle the secondary sidebar in all display breakpoints (#6118)
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-12-22 14:07:11 -08:00
Muhsin KelothandGitHub dbb6c0a074 chore: Increase the max concurrent number of devices (#6121) 2022-12-22 19:13:54 +05:30
Sivin VargheseandGitHub 8c88344170 chore: Helpcenter improvements (#6098) 2022-12-22 18:51:24 +05:30
6a78254701 feat: Send audio longer than 10 seconds and Add Prop audio-record-format (#6108)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-12-22 13:36:03 +05:30
Pranav Raj SandGitHub 26ada8b342 fix: Update the logic to show read status for web (#6107) 2022-12-21 09:58:56 -08:00
Sivin VargheseandGitHub 3c6bd2c8fd fix: Use canned response menu from the editor in contact messages (#6109) 2022-12-21 16:01:50 +05:30
Sivin VargheseandGitHub 2c2c47d7fd chore: Hide inbox name only has one inbox (#6115) 2022-12-21 13:31:53 +05:30
Sojan 34f7405689 Merge branch 'release/2.12.0' into develop 2022-12-19 22:48:01 +05:30
581 changed files with 7291 additions and 2772 deletions
-1
View File
@@ -11,7 +11,6 @@ storage
public/system
tmp
.codeclimate.yml
public/assets
public/packs
node_modules
vendor/bundle
+11 -2
View File
@@ -35,7 +35,7 @@ REDIS_SENTINELS=
REDIS_SENTINEL_MASTER_NAME=
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
# Use the following environment variable to customize passwords for sentinels.
# Use the following environment variable to customize passwords for sentinels.
# Use empty string if sentinels are configured with out passwords
# REDIS_SENTINEL_PASSWORD=
@@ -45,7 +45,7 @@ REDIS_SENTINEL_MASTER_NAME=
# REDIS_OPENSSL_VERIFY_MODE=none
# Postgres Database config variables
# You can leave POSTGRES_DATABASE blank. The default name of
# You can leave POSTGRES_DATABASE blank. The default name of
# the database in the production environment is chatwoot_production
# POSTGRES_DATABASE=
POSTGRES_HOST=postgres
@@ -213,3 +213,12 @@ STRIPE_WEBHOOK_SECRET=
# Set to true if you want to upload files to cloud storage using the signed url
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
DIRECT_UPLOADS_ENABLED=
#MS OAUTH creds
AZURE_APP_ID=
AZURE_APP_SECRET=
## Advanced configurations
## Change these values to fine tune performance
# control the concurrency setting of sidekiq
# SIDEKIQ_CONCURRENCY=10
+6
View File
@@ -37,6 +37,8 @@ gem 'json_schemer'
gem 'rack-attack'
# a utility tool for streaming, flexible and safe downloading of remote files
gem 'down', '~> 5.0'
# authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
@@ -160,6 +162,8 @@ group :test do
gem 'database_cleaner'
# mock http calls
gem 'webmock'
# test profiling
gem 'test-prof'
end
group :development, :test do
@@ -186,3 +190,5 @@ group :development, :test do
gem 'spring'
gem 'spring-watcher-listen'
end
# worked with microsoft refresh token
gem 'omniauth-oauth2'
+28 -2
View File
@@ -249,6 +249,8 @@ GEM
gli (2.21.0)
globalid (1.0.0)
activesupport (>= 5.0)
gmail_xoauth (0.4.2)
oauth (>= 0.3.6)
google-apis-core (0.7.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
@@ -333,8 +335,8 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
httparty (0.20.0)
mime-types (~> 3.0)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.11.0)
@@ -437,6 +439,20 @@ GEM
nokogiri (1.13.10-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
omniauth (2.1.0)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
orm_adapter (0.5.0)
os (1.1.4)
parallel (1.22.1)
@@ -465,6 +481,8 @@ GEM
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-protection (3.0.5)
rack
rack-proxy (0.7.2)
rack
rack-test (2.0.2)
@@ -620,6 +638,9 @@ GEM
gli
hashie
websocket-driver
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
spring (2.1.1)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
@@ -635,6 +656,7 @@ GEM
statsd-ruby (1.5.0)
stripe (6.5.0)
telephone_number (1.4.16)
test-prof (1.0.11)
thor (1.2.1)
tilt (2.0.10)
time_diff (0.3.0)
@@ -663,6 +685,7 @@ GEM
valid_email2 (4.0.3)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.1)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
@@ -735,6 +758,7 @@ DEPENDENCIES
flag_shih_tzu
foreman
geocoder
gmail_xoauth
google-cloud-dialogflow
google-cloud-storage
groupdate
@@ -756,6 +780,7 @@ DEPENDENCIES
maxminddb
mock_redis
newrelic_rpm
omniauth-oauth2
pg
pg_search
procore-sift
@@ -791,6 +816,7 @@ DEPENDENCIES
squasher
stripe
telephone_number
test-prof
time_diff
twilio-ruby (~> 5.66)
twitty
+2 -1
View File
@@ -1,3 +1,4 @@
backend: bin/rails s -p 3000
frontend: bin/webpack-dev-server
worker: bundle exec sidekiq -C config/sidekiq.yml
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
+1 -1
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050
frontend: bin/webpack-dev-server
worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
+1 -1
View File
@@ -119,4 +119,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2022, Chatwoot Inc - Released under the MIT License.
*Chatwoot* &copy; 2017-2023, Chatwoot Inc - Released under the MIT License.
@@ -0,0 +1,48 @@
class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:create_a_meeting]
before_action :fetch_message, only: [:add_participant_to_meeting]
before_action :authorize_request
def create_a_meeting
render_response(dyte_processor_service.create_a_meeting(Current.user))
end
def add_participant_to_meeting
if @message.content_type != 'integrations'
return render json: {
error: I18n.t('errors.dyte.invalid_message_type')
}, status: :unprocessable_entity
end
render_response(
dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user)
)
end
private
def authorize_request
authorize @conversation.inbox, :show?
end
def render_response(response)
render json: response, status: response[:error].blank? ? :ok : :unprocessable_entity
end
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: Current.account, conversation: @conversation)
end
def permitted_params
params.permit(:conversation_id, :message_id)
end
def fetch_conversation
@conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id])
end
def fetch_message
@message = Current.account.messages.find(permitted_params[:message_id])
@conversation = @message.conversation
end
end
@@ -0,0 +1,27 @@
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController
include MicrosoftConcern
before_action :check_authorization
def create
email = params[:authorization][:email]
redirect_url = microsoft_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/microsoft/callback",
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid',
prompt: 'consent'
}
)
if redirect_url
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -0,0 +1,36 @@
class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseController
before_action :set_message
def add_participant_to_meeting
if @message.content_type != 'integrations'
return render json: {
error: I18n.t('errors.dyte.invalid_message_type')
}, status: :unprocessable_entity
end
response = dyte_processor_service.add_participant_to_meeting(
@message.content_attributes['data']['meeting_id'],
@conversation.contact
)
render_response(response)
end
private
def render_response(response)
render json: response, status: response[:error].blank? ? :ok : :unprocessable_entity
end
def dyte_processor_service
Integrations::Dyte::ProcessorService.new(account: @web_widget.inbox.account, conversation: @conversation)
end
def set_message
@message = @web_widget.inbox.messages.find(permitted_params[:message_id])
@conversation = @message.conversation
end
def permitted_params
params.permit(:website_token, :message_id)
end
end
@@ -0,0 +1,22 @@
module MicrosoftConcern
extend ActiveSupport::Concern
def microsoft_client
::OAuth2::Client.new(ENV.fetch('AZURE_APP_ID', nil), ENV.fetch('AZURE_APP_SECRET', nil),
{
site: 'https://login.microsoftonline.com',
authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
token_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
})
end
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
end
+2 -1
View File
@@ -55,7 +55,8 @@ class DashboardController < ActionController::Base
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', '')
}
end
end
@@ -0,0 +1,72 @@
class Microsoft::CallbacksController < ApplicationController
include MicrosoftConcern
def show
@response = microsoft_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/microsoft/callback"
)
inbox = find_or_create_inbox
::Redis::Alfred.delete(users_data['email'])
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/'
end
private
def oauth_code
params[:code]
end
def users_data
decoded_token = JWT.decode parsed_body[:id_token], nil, false
decoded_token[0]
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def account_id
::Redis::Alfred.get(users_data['email'])
end
def account
@account ||= Account.find(account_id)
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
channel_email ||= create_microsoft_channel_with_inbox
update_microsoft_channel(channel_email)
channel_email.inbox
end
def create_microsoft_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name']
)
channel_email
end
end
def update_microsoft_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: 'outlook.office365.com',
imap_port: '993', imap_enabled: true,
provider: 'microsoft',
provider_config: {
access_token: parsed_body['access_token'],
refresh_token: parsed_body['refresh_token'],
expires_on: (Time.current.utc + 1.hour).to_s
}
})
end
end
+2 -2
View File
@@ -11,8 +11,8 @@ class SwaggerController < ApplicationController
def derived_path
params[:path] ||= 'index.html'
path = params[:path]
path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s)
path = Rack::Utils.clean_path_info(params[:path])
path << ".#{Rack::Utils.clean_path_info(params[:format])}" unless path.ends_with?(params[:format].to_s)
path
end
end
+17
View File
@@ -0,0 +1,17 @@
class ContactDrop < BaseDrop
def email
@obj.try(:email)
end
def phone_number
@obj.try(:phone_number)
end
def first_name
@obj.try(:name).try(:split).try(:first)
end
def last_name
@obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1
end
end
+8
View File
@@ -2,4 +2,12 @@ class UserDrop < BaseDrop
def available_name
@obj.try(:available_name)
end
def first_name
@obj.try(:name).try(:split).try(:first)
end
def last_name
@obj.try(:name).try(:split).try(:last) if @obj.try(:name).try(:split).try(:size) > 1
end
end
@@ -0,0 +1,14 @@
/* global axios */
import ApiClient from '../ApiClient';
class MicrosoftClient extends ApiClient {
constructor() {
super('microsoft', { accountScoped: true });
}
generateAuthorization(payload) {
return axios.post(`${this.url}/authorization`, payload);
}
}
export default new MicrosoftClient();
@@ -0,0 +1,23 @@
/* global axios */
import ApiClient from '../ApiClient';
class DyteAPI extends ApiClient {
constructor() {
super('integrations/dyte', { accountScoped: true });
}
createAMeeting(conversationId) {
return axios.post(`${this.url}/create_a_meeting`, {
conversation_id: conversationId,
});
}
addParticipantToMeeting(messageId) {
return axios.post(`${this.url}/add_participant_to_meeting`, {
message_id: messageId,
});
}
}
export default new DyteAPI();
@@ -0,0 +1,35 @@
import DyteAPIClient from '../../integrations/dyte';
import ApiClient from '../../ApiClient';
import describeWithAPIMock from '../apiSpecHelper';
describe('#accountAPI', () => {
it('creates correct instance', () => {
expect(DyteAPIClient).toBeInstanceOf(ApiClient);
expect(DyteAPIClient).toHaveProperty('createAMeeting');
expect(DyteAPIClient).toHaveProperty('addParticipantToMeeting');
});
describeWithAPIMock('createAMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.createAMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/create_a_meeting',
{
conversation_id: 1,
}
);
});
});
describeWithAPIMock('addParticipantToMeeting', context => {
it('creates a valid request', () => {
DyteAPIClient.addParticipantToMeeting(1);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/integrations/dyte/add_participant_to_meeting',
{
message_id: 1,
}
);
});
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -75,7 +75,7 @@ Arial,
sans-serif;
$body-antialiased: true;
$global-margin: $space-small;
$global-padding: $space-micro;
$global-padding: $space-small;
$global-weight-normal: normal;
$global-weight-bold: bold;
$global-radius: 0;
@@ -37,7 +37,6 @@
@include foundation-prototype-sizing;
@include foundation-prototype-spacing;
@import 'typography';
@import 'layout';
@import 'animations';
@@ -61,7 +60,6 @@
@import 'widgets/woot-tables';
@import 'views/settings/inbox';
@import 'views/settings/channel';
@import 'views/settings/integrations';
@import 'plugins/multiselect';
@@ -1,52 +0,0 @@
$channel-hover-color: rgba(0, 0, 0, 0.1);
.channels {
margin-top: $space-medium;
.inactive {
filter: grayscale(100%);
}
.channel {
@include flex;
@include background-white;
@include border-light;
cursor: pointer;
flex-direction: column;
margin: -1px;
padding: $space-normal $zero;
transition: all 0.2s ease-in;
&:last-child {
@include border-light;
}
&:hover {
border: 1px solid $primary-color;
box-shadow: 0 2px 8px $channel-hover-color;
z-index: 999;
}
&.disabled {
opacity: 0.6;
}
img {
margin: $space-normal auto;
width: 50%;
}
.channel__title {
color: $color-body;
font-size: var(--font-size-default);
text-align: center;
text-transform: capitalize;
}
p {
color: $medium-gray;
width: 100%;
}
}
}
@@ -42,7 +42,6 @@ $resolve-button-width: 13.2rem;
margin-right: var(--space-normal);
min-width: 0;
.user--profile__meta {
align-items: flex-start;
display: flex;
@@ -54,13 +53,17 @@ $resolve-button-width: 13.2rem;
}
}
.header-actions-wrap {
align-items: center;
display: flex;
flex-direction: row;
flex-grow: 1;
justify-content: flex-end;
margin-top: var(--space-small);
@include breakpoint(medium up) {
margin-top: 0;
}
&.has-open-sidebar {
justify-content: flex-end;
@@ -81,8 +81,12 @@
}
.conversations-list {
@include scroll-on-hover;
overflow-y: auto;
flex: 1 1;
@include breakpoint(large up) {
@include scroll-on-hover;
}
}
.chat-list__top {
@@ -324,6 +328,7 @@
var(--space-one);
.is-text {
align-items: center;
display: inline-flex;
text-align: start;
@@ -82,8 +82,8 @@
@include flex-align($x: flex-end, $y: middle);
padding: $space-small $zero;
button {
font-size: $font-size-small;
.button {
margin-left: var(--space-small);
}
&.justify-content-end {
@@ -59,12 +59,8 @@
.hamburger--menu {
cursor: pointer;
display: none;
display: block;
margin-right: $space-normal;
@media screen and (max-width: 1200px) {
display: block;
}
}
.header--icon {
@@ -0,0 +1,67 @@
<template>
<button class="small-6 medium-4 large-3 channel" @click="$emit('click')">
<img :src="src" :alt="title" />
<h3 class="channel__title">
{{ title }}
</h3>
</button>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
src: {
type: String,
required: true,
},
},
};
</script>
<style scoped lang="scss">
.inactive {
filter: grayscale(100%);
}
.channel {
background: var(--white);
border: 1px solid var(--color-border-light);
cursor: pointer;
display: flex;
flex-direction: column;
margin: -1px;
padding: var(--space-normal) 0;
transition: all 0.2s ease-in;
align-items: center;
&:hover {
border: 1px solid var(--w-500);
box-shadow: var(--shadow-medium);
z-index: var(--z-index-high);
}
&.disabled {
opacity: 0.6;
}
img {
margin: var(--space-normal) auto;
width: 50%;
}
.channel__title {
color: var(--color-body);
font-size: var(--font-size-default);
text-align: center;
text-transform: capitalize;
}
p {
color: var(--b-500);
width: 100%;
}
}
</style>
@@ -794,30 +794,15 @@ export default {
.conversations-list-wrap {
flex-shrink: 0;
width: 34rem;
flex-basis: clamp(32rem, 4vw + 34rem, 44rem);
overflow: hidden;
@include breakpoint(large up) {
width: 36rem;
}
@include breakpoint(xlarge up) {
width: 35rem;
}
@include breakpoint(xxlarge up) {
width: 38rem;
}
@include breakpoint(xxxlarge up) {
flex-basis: 46rem;
}
&.hide {
display: none;
}
&.list--full-width {
width: 100%;
@include breakpoint(xxxlarge up) {
flex-basis: 100%;
}
flex-basis: 100%;
}
}
.filter--actions {
@@ -39,6 +39,8 @@ export default {
<style scoped lang="scss">
.wrap-content {
word-wrap: break-word;
margin-top: var(--space-small);
.content-value {
font-weight: var(--font-weight-bold);
}
@@ -1,7 +1,12 @@
<template>
<button @click="onMenuItemClick">
<fluent-icon class="hamburger--menu" icon="list" />
</button>
<woot-button
size="small"
variant="clear"
color-scheme="secondary"
icon="list"
class="toggle-sidebar"
@click="onMenuItemClick"
/>
</template>
<script>
@@ -16,13 +21,8 @@ export default {
};
</script>
<style scoped lang="scss">
.hamburger--menu {
cursor: pointer;
display: none;
margin-right: var(--space-normal);
@media screen and (max-width: 1200px) {
display: block;
}
.toggle-sidebar {
margin-right: var(--space-small);
margin-left: var(--space-minus-small);
}
</style>
@@ -233,9 +233,6 @@ export default {
width: 40rem;
}
}
.off-canvas-content.is-open-left {
transform: translateX(18.8rem);
}
.secondary-sidebar {
overflow-y: auto;
@@ -261,14 +261,7 @@ export default {
width: 20rem;
flex-shrink: 0;
overflow-y: hidden;
@include breakpoint(xlarge down) {
position: absolute;
}
@include breakpoint(xlarge up) {
position: unset;
}
position: unset;
&:hover {
overflow-y: hidden;
@@ -1,10 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidemenuIcon matches snapshot 1`] = `
<button>
<fluent-icon
class="hamburger--menu"
icon="list"
/>
</button>
<woot-button
class="toggle-sidebar"
color-scheme="secondary"
icon="list"
size="small"
variant="clear"
/>
`;
@@ -88,9 +88,9 @@
v-model="action_params"
:teams="dropdownValues"
/>
<textarea
<woot-message-editor
v-if="inputType === 'textarea'"
v-model="action_params"
v-model="castMessageVmodel"
rows="4"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
class="action-message"
@@ -107,10 +107,12 @@
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
export default {
components: {
AutomationActionTeamMessageInput,
AutomationActionFileInput,
WootMessageEditor,
},
props: {
value: {
@@ -173,6 +175,17 @@ export default {
'is-a-macro': this.isMacro,
};
},
castMessageVmodel: {
get() {
if (Array.isArray(this.action_params)) {
return this.action_params[0];
}
return this.action_params;
},
set(value) {
this.action_params = value;
},
},
},
methods: {
removeAction() {
@@ -281,4 +294,8 @@ export default {
.action-message {
margin: var(--space-small) var(--space-zero) var(--space-zero);
}
// Prosemirror does not have a native way of hiding the menu bar, hence
::v-deep .ProseMirror-menubar {
display: none;
}
</style>
@@ -1,56 +1,15 @@
<template>
<div
class="small-6 medium-4 large-3 columns channel"
<channel-selector
:class="{ inactive: !isActive }"
:title="channel.name"
:src="getChannelThumbnail()"
@click="onItemClick"
>
<img
v-if="channel.key === 'facebook'"
src="~dashboard/assets/images/channels/messenger.png"
/>
<img
v-if="channel.key === 'twitter'"
src="~dashboard/assets/images/channels/twitter.png"
/>
<img
v-if="channel.key === 'telegram'"
src="~dashboard/assets/images/channels/telegram.png"
/>
<img
v-if="channel.key === 'api' && !channel.thumbnail"
src="~dashboard/assets/images/channels/api.png"
/>
<img
v-if="channel.key === 'api' && channel.thumbnail"
:src="channel.thumbnail"
/>
<img
v-if="channel.key === 'email'"
src="~dashboard/assets/images/channels/email.png"
/>
<img
v-if="channel.key === 'line'"
src="~dashboard/assets/images/channels/line.png"
/>
<img
v-if="channel.key === 'website'"
src="~dashboard/assets/images/channels/website.png"
/>
<img
v-if="channel.key === 'sms'"
src="~dashboard/assets/images/channels/sms.png"
/>
<img
v-if="channel.key === 'whatsapp'"
src="~dashboard/assets/images/channels/whatsapp.png"
/>
<h3 class="channel__title">
{{ channel.name }}
</h3>
</div>
/>
</template>
<script>
import ChannelSelector from '../ChannelSelector';
export default {
components: { ChannelSelector },
props: {
channel: {
type: Object,
@@ -92,6 +51,12 @@ export default {
},
},
methods: {
getChannelThumbnail() {
if (this.channel.key === 'api' && this.channel.thumbnail) {
return this.channel.thumbnail;
}
return `/assets/images/dashboard/channels/${this.channel.key}.png`;
},
onItemClick() {
if (this.isActive) {
this.$emit('channel-item-click', this.channel.key);
@@ -0,0 +1,57 @@
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="
$t('INTEGRATION_SETTINGS.DYTE.START_VIDEO_CALL_HELP_TEXT')
"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
@click="onClick"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: Number,
default: 0,
},
},
data() {
return { isLoading: false };
},
computed: {
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
isVideoIntegrationEnabled() {
return this.appIntegrations.find(
integration => integration.id === 'dyte' && !!integration.hooks.length
);
},
},
mounted() {
if (!this.appIntegrations.length) {
this.$store.dispatch('integrations/get');
}
},
methods: {
async onClick() {
this.isLoading = true;
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally {
this.isLoading = false;
}
},
},
};
</script>
@@ -10,11 +10,11 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import inboxMixin from '../../../../shared/mixins/inboxMixin';
import alertMixin from '../../../../shared/mixins/alertMixin';
import Recorder from 'opus-recorder';
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import waveWorker from 'opus-recorder/dist/waveWorker.min';
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
@@ -29,14 +29,19 @@ WaveSurfer.microphone = MicrophonePlugin;
export default {
name: 'WootAudioRecorder',
mixins: [inboxMixin, alertMixin],
mixins: [alertMixin],
props: {
audioRecordFormat: {
type: String,
default: AUDIO_FORMATS.WEBM,
},
},
data() {
return {
player: false,
recordingDateStarted: new Date(0),
initialTimeDuration: '00:00',
recorderOptions: {
debug: true,
controls: true,
bigPlayButton: false,
fluid: false,
@@ -71,6 +76,9 @@ export default {
record: {
audio: true,
video: false,
maxLength: 900,
timeSlice: 1000,
maxFileSize: 15 * 1024 * 1024,
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && {
monitorGain: 0,
recordingGain: 1,
@@ -80,11 +88,10 @@ export default {
streamPages: true,
maxFramesPerPage: 1,
encoderFrameSize: 1,
encoderPath: 'opus-recorder/dist/waveWorker.min.js',
encoderPath: waveWorker,
}),
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
displayMilliseconds: false,
maxLength: 300,
audioEngine: 'opus-recorder',
audioWorkerURL: encoderWorker,
audioChannels: 1,
@@ -100,12 +107,6 @@ export default {
isRecording() {
return this.player && this.player.record().isRecording();
},
audioRecordFormat() {
if (this.isAWebWidgetInbox) {
return AUDIO_FORMATS.WEBM;
}
return AUDIO_FORMATS.OGG;
},
},
mounted() {
window.Recorder = Recorder;
@@ -15,29 +15,25 @@
</template>
<script>
import { EditorView } from 'prosemirror-view';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import {
addMentionsToMarkdownSerializer,
addMentionsToMarkdownParser,
schemaWithMentions,
} from '@chatwoot/prosemirror-schema/src/mentions/schema';
messageSchema,
wootMessageWriterSetup,
EditorView,
MessageMarkdownTransformer,
MessageMarkdownSerializer,
EditorState,
Selection,
} from '@chatwoot/prosemirror-schema';
import {
suggestionsPlugin,
triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import { EditorState, Selection } from 'prosemirror-state';
import { defaultMarkdownParser } from 'prosemirror-markdown';
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
import TagAgents from '../conversation/TagAgents';
import CannedResponse from '../conversation/CannedResponse';
const TYPING_INDICATOR_IDLE_TIME = 4000;
import '@chatwoot/prosemirror-schema/src/woot-editor.css';
import {
hasPressedEnterAndNotCmdOrShift,
hasPressedCommandAndEnter,
@@ -53,9 +49,9 @@ import AnalyticsHelper, {
const createState = (content, placeholder, plugins = []) => {
return EditorState.create({
doc: addMentionsToMarkdownParser(defaultMarkdownParser).parse(content),
plugins: wootWriterSetup({
schema: schemaWithMentions,
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
plugins: wootMessageWriterSetup({
schema: messageSchema,
placeholder,
plugins,
}),
@@ -88,9 +84,7 @@ export default {
},
computed: {
contentFromEditor() {
return addMentionsToMarkdownSerializer(
defaultMarkdownSerializer
).serialize(this.editorView.state.doc);
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
},
plugins() {
if (!this.enableSuggestions) {
@@ -282,11 +276,11 @@ export default {
}
let from = this.range.from - 1;
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
let node = new MessageMarkdownTransformer(messageSchema).parse(
cannedItem
);
if (node.childCount === 1) {
if (node.textContent === cannedItem) {
node = this.editorView.state.schema.text(cannedItem);
from = this.range.from;
}
@@ -372,10 +366,18 @@ export default {
</script>
<style lang="scss">
@import '~@chatwoot/prosemirror-schema/src/styles/base.scss';
.ProseMirror-menubar-wrapper {
display: flex;
flex-direction: column;
.ProseMirror-menubar {
min-height: var(--space-two) !important;
margin-left: var(--space-minus-one);
padding-bottom: 0;
}
> .ProseMirror {
padding: 0;
word-break: break-word;
@@ -384,6 +386,7 @@ export default {
.editor-root {
width: 100%;
position: relative;
}
.ProseMirror-woot-style {
@@ -406,6 +409,9 @@ export default {
color: var(--s-900);
padding: 0 var(--space-smaller);
}
.ProseMirror-menubar {
background: var(--y-50);
}
}
.editor-wrap {
@@ -0,0 +1,167 @@
<template>
<div>
<div class="editor-root editor--article">
<div ref="editor" />
</div>
</div>
</template>
<script>
import {
fullSchema,
wootArticleWriterSetup,
EditorView,
ArticleMarkdownSerializer,
ArticleMarkdownTransformer,
EditorState,
Selection,
} from '@chatwoot/prosemirror-schema';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
const createState = (content, placeholder, plugins = []) => {
return EditorState.create({
doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
plugins: wootArticleWriterSetup({
schema: fullSchema,
placeholder,
plugins,
}),
});
};
export default {
mixins: [eventListenerMixins, uiSettingsMixin],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
editorView: null,
state: undefined,
plugins: [],
};
},
computed: {
contentFromEditor() {
if (this.editorView) {
return ArticleMarkdownSerializer.serialize(this.editorView.state.doc);
}
return '';
},
},
watch: {
value(newValue = '') {
if (newValue !== this.contentFromEditor) {
this.reloadState();
}
},
editorId() {
this.reloadState();
},
},
created() {
this.state = createState(this.value, this.placeholder, this.plugins);
},
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditorInputField();
},
methods: {
reloadState() {
this.state = createState(this.value, this.placeholder, this.plugins);
this.editorView.updateState(this.state);
this.focusEditorInputField();
},
createEditorView() {
this.editorView = new EditorView(this.$refs.editor, {
state: this.state,
dispatchTransaction: tx => {
this.state = this.state.apply(tx);
this.emitOnChange();
},
handleDOMEvents: {
keyup: () => {
this.onKeyup();
},
keydown: (view, event) => {
this.onKeydown(event);
},
focus: () => {
this.onFocus();
},
blur: () => {
this.onBlur();
},
},
});
},
handleKeyEvents() {},
focusEditorInputField() {
const { tr } = this.editorView.state;
const selection = Selection.atEnd(tr.doc);
this.editorView.dispatch(tr.setSelection(selection));
this.editorView.focus();
},
emitOnChange() {
this.editorView.updateState(this.state);
this.$emit('input', this.contentFromEditor);
},
onKeyup() {
this.$emit('keyup');
},
onKeydown() {
this.$emit('keydown');
},
onBlur() {
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
},
},
};
</script>
<style lang="scss">
@import '~@chatwoot/prosemirror-schema/src/styles/article.scss';
.ProseMirror-menubar-wrapper {
display: flex;
flex-direction: column;
> .ProseMirror {
padding: 0;
word-break: break-word;
}
}
.editor-root {
width: 100%;
}
.ProseMirror-woot-style {
min-height: 8rem;
max-height: 12rem;
overflow: auto;
}
.ProseMirror-prompt {
z-index: var(--z-index-highest);
background: var(--white);
box-shadow: var(--shadow-large);
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
min-width: 40rem;
}
</style>
@@ -87,6 +87,10 @@
:title="'Whatsapp Templates'"
@click="$emit('selectWhatsappTemplate')"
/>
<video-call-button
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
:conversation-id="conversationId"
/>
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
@@ -124,13 +128,13 @@ import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton';
import { REPLY_EDITOR_MODES } from './constants';
import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload },
components: { FileUpload, VideoCallButton },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
mode: {
@@ -209,6 +213,10 @@ export default {
type: Boolean,
default: false,
},
conversationId: {
type: Number,
required: true,
},
},
computed: {
...mapGetters({
@@ -269,7 +277,7 @@ export default {
}
},
showMessageSignatureButton() {
return !this.isPrivate && this.isAnEmailChannel;
return !this.isOnPrivateNote && this.isAnEmailChannel;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
@@ -166,10 +166,14 @@ export default {
.conversation-sidebar-wrap {
height: auto;
flex: 0 0;
overflow: hidden;
z-index: var(--z-index-low);
overflow: auto;
background: white;
flex-basis: 28rem;
flex-basis: 100%;
@include breakpoint(medium up) {
flex-basis: 28rem;
}
@include breakpoint(large up) {
flex-basis: 30em;
@@ -1,49 +1,54 @@
<template>
<div class="conv-header">
<div class="user">
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
<Thumbnail
:src="currentContact.thumbnail"
size="40px"
:badge="inboxBadge"
:username="currentContact.name"
:status="currentContact.availability_status"
/>
<div class="user--profile__meta">
<h3 class="user--name text-truncate">
<span class="margin-right-smaller">{{ currentContact.name }}</span>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="hmac-warning__icon"
icon="warning"
/>
</h3>
<div class="conversation--header--actions">
<inbox-name :inbox="inbox" class="margin-right-small" />
<span
v-if="isSnoozed"
class="snoozed--display-text margin-right-small"
>
{{ snoozedDisplayText }}
</span>
<woot-button
class="user--profile__button margin-right-small"
size="small"
variant="link"
@click="$emit('contact-panel-toggle')"
>
{{ contactPanelToggleText }}
</woot-button>
<div class="conversation-header--details">
<div class="user">
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
<Thumbnail
:src="currentContact.thumbnail"
:badge="inboxBadge"
:username="currentContact.name"
:status="currentContact.availability_status"
/>
<div class="user--profile__meta">
<h3 class="user--name text-truncate">
<span class="margin-right-smaller">{{ currentContact.name }}</span>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="hmac-warning__icon"
icon="warning"
/>
</h3>
<div class="conversation--header--actions text-truncate">
<inbox-name
v-if="hasMultipleInboxes"
:inbox="inbox"
class="margin-right-small"
/>
<span
v-if="isSnoozed"
class="snoozed--display-text margin-right-small"
>
{{ snoozedDisplayText }}
</span>
<woot-button
class="user--profile__button margin-right-small"
size="small"
variant="link"
@click="$emit('contact-panel-toggle')"
>
{{ contactPanelToggleText }}
</woot-button>
</div>
</div>
</div>
</div>
<div
class="header-actions-wrap"
:class="{ 'has-open-sidebar': isContactPanelOpen }"
>
<more-actions :conversation-id="currentChat.id" />
<div
class="header-actions-wrap"
:class="{ 'has-open-sidebar': isContactPanelOpen }"
>
<more-actions :conversation-id="currentChat.id" />
</div>
</div>
</div>
</template>
@@ -145,6 +150,9 @@ export default {
const { inbox_id: inboxId } = this.chat;
return this.$store.getters['inboxes/getInbox'](inboxId);
},
hasMultipleInboxes() {
return this.$store.getters['inboxes/getInboxes'].length > 1;
},
},
methods: {
@@ -158,14 +166,27 @@ export default {
</script>
<style lang="scss" scoped>
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@import '~dashboard/assets/scss/woot';
.conv-header {
flex: 0 0 var(--space-jumbo);
flex-direction: row;
@include breakpoint(medium up) {
flex-direction: column;
}
}
.conversation-header--details {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
width: 100%;
@include breakpoint(medium up) {
flex-direction: row;
}
}
.option__desc {
@@ -1,8 +1,5 @@
<template>
<li
v-if="hasAttachments || data.content || isEmailContentType"
:class="alignBubble"
>
<li v-if="shouldRenderMessage" :class="alignBubble">
<div :class="wrapClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head
@@ -15,9 +12,13 @@
v-if="data.content"
:message="message"
:is-email="isEmailContentType"
:readable-time="readableTime"
:display-quoted-button="displayQuotedButton"
/>
<bubble-integration
:message-id="data.id"
:content-attributes="contentAttributes"
:inbox-id="data.inbox_id"
/>
<span
v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent"
@@ -29,7 +30,6 @@
<bubble-image
v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url"
:readable-time="readableTime"
@error="onImageLoadError"
/>
<audio v-else-if="attachment.file_type === 'audio'" controls>
@@ -38,7 +38,6 @@
<bubble-video
v-else-if="attachment.file_type === 'video'"
:url="attachment.data_url"
:readable-time="readableTime"
/>
<bubble-location
v-else-if="attachment.file_type === 'location'"
@@ -46,11 +45,7 @@
:longitude="attachment.coordinates_long"
:name="attachment.fallback_title"
/>
<bubble-file
v-else
:url="attachment.data_url"
:readable-time="readableTime"
/>
<bubble-file v-else :url="attachment.data_url" />
</div>
</div>
<bubble-actions
@@ -65,10 +60,9 @@
:is-private="data.private"
:message-type="data.message_type"
:message-status="status"
:readable-time="readableTime"
:source-id="data.source_id"
:inbox-id="data.inbox_id"
:message-read="showReadTicks"
:created-at="createdAt"
/>
</div>
<spinner v-if="isPending" size="tiny" />
@@ -119,16 +113,14 @@
</template>
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import timeMixin from '../../../mixins/time';
import BubbleActions from './bubble/Actions';
import BubbleFile from './bubble/File';
import BubbleImage from './bubble/Image';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location';
import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text';
import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File';
import BubbleVideo from './bubble/Video.vue';
import BubbleActions from './bubble/Actions';
import BubbleLocation from './bubble/Location';
import Spinner from 'shared/components/Spinner';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
@@ -140,16 +132,17 @@ import { generateBotMessageContent } from './helpers/botMessageContentHelper';
export default {
components: {
BubbleActions,
BubbleText,
BubbleImage,
BubbleFile,
BubbleVideo,
BubbleMailHead,
BubbleImage,
BubbleIntegration,
BubbleLocation,
BubbleMailHead,
BubbleText,
BubbleVideo,
ContextMenu,
Spinner,
},
mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: {
data: {
type: Object,
@@ -167,10 +160,6 @@ export default {
type: Boolean,
default: false,
},
hasUserReadMessage: {
type: Boolean,
default: false,
},
isWebWidgetInbox: {
type: Boolean,
default: false,
@@ -183,6 +172,14 @@ export default {
};
},
computed: {
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isAnIntegrationMessage
);
},
emailMessageContent() {
const {
html_content: { full: fullHTMLContent } = {},
@@ -273,11 +270,8 @@ export default {
'has-tweet-menu': this.isATweet,
};
},
readableTime() {
return this.messageStamp(
this.contentAttributes.external_created_at || this.data.created_at,
'LLL d, h:mm a'
);
createdAt() {
return this.contentAttributes.external_created_at || this.data.created_at;
},
isBubble() {
return [0, 1, 3].includes(this.data.message_type);
@@ -288,17 +282,12 @@ export default {
isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
},
showReadTicks() {
return (
(this.isOutgoing || this.isTemplate) &&
this.hasUserReadMessage &&
this.isWebWidgetInbox &&
!this.data.private
);
},
isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
},
isAnIntegrationMessage() {
return this.contentType === 'integrations';
},
emailHeadAttributes() {
return {
email: this.contentAttributes.email,
@@ -40,9 +40,6 @@
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
@@ -63,9 +60,6 @@
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/>
</ul>
@@ -37,6 +37,7 @@
<woot-audio-recorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@state-recorder-progress-changed="onStateProgressRecorderChanged"
@state-recorder-changed="onStateRecorderChanged"
@finish-record="onFinishRecorder"
@@ -95,25 +96,26 @@
</p>
</div>
<reply-bottom-panel
:mode="replyType"
:inbox="inbox"
:send-button-text="replyButtonLabel"
:on-file-upload="onFileUpload"
:show-file-upload="showFileUpload"
:show-audio-recorder="showAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:show-emoji-picker="showEmojiPicker"
:on-send="onSendReply"
:is-send-disabled="isReplyButtonDisabled"
:recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState"
:is-recording-audio="isRecordingAudio"
:is-on-private-note="isOnPrivateNote"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:conversation-id="conversationId"
:enable-multiple-file-upload="enableMultipleFileUpload"
:has-whatsapp-templates="hasWhatsappTemplates"
:inbox="inbox"
:is-on-private-note="isOnPrivateNote"
:is-recording-audio="isRecordingAudio"
:is-send-disabled="isReplyButtonDisabled"
:mode="replyType"
:on-file-upload="onFileUpload"
:on-send="onSendReply"
:recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState"
:send-button-text="replyButtonLabel"
:show-audio-recorder="showAudioRecorder"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:show-emoji-picker="showEmojiPicker"
:show-file-upload="showFileUpload"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
/>
@@ -147,6 +149,7 @@ import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import {
MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
AUDIO_FORMATS,
} from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -462,6 +465,12 @@ export default {
editorStateId() {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (this.isAWebWidgetInbox) {
return AUDIO_FORMATS.WEBM;
}
return AUDIO_FORMATS.OGG;
},
},
watch: {
currentChat(conversation) {
@@ -96,7 +96,9 @@ export default {
<style scoped lang="scss">
.mention--box {
background: var(--white);
border-radius: var(--border-radius-normal);
border-top: 1px solid var(--color-border);
box-shadow: var(--shadow-medium);
font-size: var(--font-size-small);
left: 0;
line-height: 1.2;
@@ -1,8 +1,14 @@
<template>
<div class="message-text--metadata">
<span class="time" :class="{ delivered: messageRead }">{{
readableTime
}}</span>
<span
class="time"
:class="{
'has-status-icon':
showSentIndicator || showDeliveredIndicator || showReadIndicator,
}"
>
{{ readableTime }}
</span>
<span v-if="showReadIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
@@ -11,7 +17,7 @@
size="14"
/>
</span>
<span v-if="showDeliveredIndicator" class="read-indicator-wrap">
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
icon="checkmark-double"
@@ -19,7 +25,7 @@
size="14"
/>
</span>
<span v-if="showSentIndicator" class="read-indicator-wrap">
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
icon="checkmark"
@@ -74,17 +80,19 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
export default {
mixins: [inboxMixin],
mixins: [inboxMixin, timeMixin],
props: {
sender: {
type: Object,
default: () => ({}),
},
readableTime: {
type: String,
default: '',
createdAt: {
type: Number,
default: 0,
},
storySender: {
type: String,
@@ -130,12 +138,9 @@ export default {
type: [String, Number],
default: 0,
},
messageRead: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({ currentChat: 'getSelectedChat' }),
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
@@ -145,6 +150,9 @@ export default {
isOutgoing() {
return MESSAGE_TYPE.OUTGOING === this.messageType;
},
isTemplate() {
return MESSAGE_TYPE.TEMPLATE === this.messageType;
},
isDelivered() {
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
},
@@ -154,6 +162,9 @@ export default {
isSent() {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageStamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
this.sender || {};
@@ -174,28 +185,52 @@ export default {
const { storySender, storyId } = this;
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
},
showStatusIndicators() {
if ((this.isOutgoing || this.isTemplate) && !this.isPrivate) {
return true;
}
return false;
},
showSentIndicator() {
return (
this.isOutgoing &&
this.sourceId &&
(this.isAnEmailChannel || (this.isAWhatsAppChannel && this.isSent))
);
if (!this.showStatusIndicators) {
return false;
}
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isSent;
}
return false;
},
showDeliveredIndicator() {
return (
this.isOutgoing &&
this.sourceId &&
this.isAWhatsAppChannel &&
this.isDelivered
);
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isDelivered;
}
return false;
},
showReadIndicator() {
return (
this.isOutgoing &&
this.sourceId &&
this.isAWhatsAppChannel &&
this.isRead
);
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWebWidgetInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isRead;
}
return false;
},
},
methods: {
@@ -218,12 +253,13 @@ export default {
.action--icon {
color: var(--white);
&.read-tick {
color: var(--v-100);
}
&.read-indicator {
color: var(--g-300);
color: var(--g-200);
}
}
@@ -288,8 +324,9 @@ export default {
position: absolute;
right: var(--space-small);
white-space: nowrap;
&.delivered {
right: var(--space-medium);
&.has-status-icon {
right: var(--space-large);
line-height: 2;
}
}
@@ -0,0 +1,39 @@
<template>
<dyte-video-call
v-if="showDyteIntegration"
:message-id="messageId"
:meeting-data="contentAttributes.data"
/>
</template>
<script>
import DyteVideoCall from './integrations/Dyte.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
export default {
components: { DyteVideoCall },
mixins: [inboxMixin],
props: {
messageId: {
type: [String, Number],
default: 0,
},
contentAttributes: {
type: Object,
default: () => ({}),
},
inboxId: {
type: [String, Number],
default: 0,
},
},
computed: {
showDyteIntegration() {
const isEnabledOnTheInbox = this.isAPIInbox || this.isAWebWidgetInbox;
return isEnabledOnTheInbox && this.contentAttributes.type === 'dyte';
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
},
};
</script>
@@ -35,10 +35,6 @@ export default {
type: String,
default: '',
},
readableTime: {
type: String,
default: '',
},
isEmail: {
type: Boolean,
default: true,
@@ -0,0 +1,103 @@
<template>
<div>
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
icon="video-add"
class="join-call-button"
:is-loading="isLoading"
@click="joinTheCall"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN') }}
</woot-button>
<div v-if="dyteAuthToken" class="video-call--container">
<iframe
:src="meetingLink"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
class="join-call-button"
@click="leaveTheRoom"
>
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
</woot-button>
</div>
</div>
</template>
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
messageId: {
type: Number,
required: true,
},
meetingData: {
type: Object,
default: () => ({}),
},
},
data() {
return { isLoading: false, dyteAuthToken: '', isSDKMounted: false };
},
computed: {
meetingLink() {
return buildDyteURL(this.meetingData.room_name, this.dyteAuthToken);
},
},
methods: {
async joinTheCall() {
this.isLoading = true;
try {
const {
data: { authResponse: { authToken } = {} } = {},
} = await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
},
leaveTheRoom() {
this.dyteAuthToken = '';
},
},
};
</script>
<style lang="scss">
.join-call-button {
margin: var(--space-small) 0;
}
.video-call--container {
position: fixed;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: var(--z-index-high);
padding: var(--space-smaller);
background: var(--b-800);
iframe {
width: 100%;
height: 100%;
border: 0;
}
button {
position: absolute;
top: var(--space-smaller);
right: 16rem;
}
}
</style>
@@ -70,7 +70,9 @@ export default {
.mention--box {
background: var(--white);
border-bottom: var(--space-small) solid var(--white);
border-radius: var(--border-radius-normal);
border-top: 1px solid var(--color-border);
box-shadow: var(--shadow-medium);
left: 0;
max-height: 14rem;
overflow: auto;
@@ -1,23 +1,14 @@
<template>
<modal :show.sync="show" :on-close="cancel">
<div class="column content-box">
<woot-modal-header :header-title="title" />
<div class="row modal-content">
<div class="medium-12 columns">
<p>
{{ description }}
</p>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button @click="confirm">
{{ confirmLabel }}
</woot-button>
<button class="button clear" @click="cancel">
{{ cancelLabel }}
</button>
</div>
</div>
<woot-modal-header :header-title="title" :header-content="description" />
<div class="modal-footer">
<woot-button variant="clear" @click="cancel">
{{ cancelLabel }}
</woot-button>
<woot-button @click="confirm">
{{ confirmLabel }}
</woot-button>
</div>
</div>
</modal>
@@ -73,3 +64,8 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.modal-container .modal-footer {
padding: var(--space-normal) var(--space-medium);
}
</style>
@@ -1,89 +1,87 @@
<template>
<transition name="slide-up">
<div class="modal-mask">
<div v-on-clickaway="() => $emit('clickaway')" class="modal-container">
<div class="header-wrap">
<div class="title-shortcut-key__wrap">
<h2 class="page-title">
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
</h2>
<div class="shortcut-key__wrap">
<p class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</p>
<p class="shortcut-key key">
<woot-modal :show="show" size="medium" :on-close="() => $emit('close')">
<div class="column content-box">
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS')"
/>
<div class="shortcut__wrap margin-top-3">
<div class="title-key__wrap">
<h5 class="text-block-title">
{{ $t('KEYBOARD_SHORTCUTS.TOGGLE_MODAL') }}
</h5>
<div class="shortcut-key__wrap">
<p class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</p>
<p class="shortcut-key key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
</p>
</div>
</div>
</div>
<div class="shortcut__wrap">
<div class="title-key__wrap">
<h5 class="text-block-title">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.OPEN_CONVERSATION') }}
</h5>
<div class="shortcut-key__wrap">
<div class="open-conversation__key">
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key">
J
</span>
<span class="forward-slash text-block-title">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
</p>
</span>
</div>
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key key">
K
</span>
</div>
<button class="cursor-pointer" @click="$emit('close')">
<fluent-icon icon="dismiss" />
</button>
</div>
<div class="shortcut__wrap">
<div class="title-key__wrap">
<span class="sub-block-title">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.OPEN_CONVERSATION') }}
<div class="title-key__wrap">
<h5 class="text-block-title">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.RESOLVE_AND_NEXT') }}
</h5>
<div class="shortcut-key__wrap">
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</span>
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key key">
E
</span>
<div class="shortcut-key__wrap">
<div class="open-conversation__key">
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key">
J
</span>
<span class="forward-slash sub-block-title">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
</span>
</div>
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key key">
K
</span>
</div>
</div>
<div class="title-key__wrap">
<span class="sub-block-title">
{{ $t('KEYBOARD_SHORTCUTS.TITLE.RESOLVE_AND_NEXT') }}
</div>
<div
v-for="shortcutKey in shortcutKeys"
:key="shortcutKey.id"
class="title-key__wrap"
>
<h5 class="text-block-title">
{{ title(shortcutKey) }}
</h5>
<div class="shortcut-key__wrap">
<span class="shortcut-key">
{{ shortcutKey.firstkey }}
</span>
<div class="shortcut-key__wrap">
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
</span>
<span class="shortcut-key">
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
</span>
<span class="shortcut-key key">
E
</span>
</div>
</div>
<div
v-for="shortcutKey in shortcutKeys"
:key="shortcutKey.id"
class="title-key__wrap"
>
<span class="sub-block-title">
{{ title(shortcutKey) }}
<span class="shortcut-key key">
{{ shortcutKey.secondKey }}
</span>
<div class="shortcut-key__wrap">
<span class="shortcut-key">
{{ shortcutKey.firstkey }}
</span>
<span class="shortcut-key key">
{{ shortcutKey.secondKey }}
</span>
</div>
</div>
</div>
</div>
</div>
</transition>
</woot-modal>
</template>
<script>
@@ -92,6 +90,12 @@ import { SHORTCUT_KEYS } from './constants';
export default {
mixins: [clickaway],
props: {
show: {
type: Boolean,
default: false,
},
},
data() {
return {
shortcutKeys: SHORTCUT_KEYS,
@@ -106,19 +110,6 @@ export default {
</script>
<style lang="scss" scoped>
.modal-container {
padding: var(--space-medium) var(--space-large) var(--space-large)
var(--space-large);
width: fit-content;
}
.header-wrap {
align-items: flex-start;
display: flex;
justify-content: space-between;
margin-bottom: var(--space-slab);
}
.title-shortcut-key__wrap {
display: flex;
}
@@ -140,6 +131,7 @@ export default {
grid-template-columns: repeat(2, 0.5fr);
gap: var(--space-smaller) var(--space-large);
margin-top: var(--space-small);
padding: 0 var(--space-large) var(--space-large);
}
.title-key__wrap {
@@ -149,11 +141,6 @@ export default {
min-width: 40rem;
}
.sub-block-title {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
.forward-slash {
display: flex;
align-items: center;
+1
View File
@@ -23,5 +23,6 @@ export default {
},
DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
SMALL_SCREEN_BREAKPOINT: 1024,
};
export const DEFAULT_REDIRECT_URL = '/app/';
@@ -0,0 +1,148 @@
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { showBadgeOnFavicon } from './faviconHelper';
import { initFaviconSwitcher } from './faviconHelper';
import {
getAlertAudio,
initOnEvents,
} from 'shared/helpers/AudioNotificationHelper';
const NOTIFICATION_TIME = 30000;
class DashboardAudioNotificationHelper {
constructor() {
this.recurringNotificationTimer = null;
this.audioAlertType = 'none';
this.playAlertOnlyWhenHidden = true;
this.alertIfUnreadConversationExist = false;
this.currentUserId = null;
this.audioAlertTone = 'ding';
}
setInstanceValues = ({
currentUserId,
alwaysPlayAudioAlert,
alertIfUnreadConversationExist,
audioAlertType,
audioAlertTone,
}) => {
this.audioAlertType = audioAlertType;
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.currentUserId = currentUserId;
this.audioAlertTone = audioAlertTone;
initOnEvents.forEach(e => {
document.addEventListener(e, this.onAudioListenEvent, false);
});
initFaviconSwitcher();
};
onAudioListenEvent = async () => {
try {
await getAlertAudio('', {
type: 'dashboard',
alertTone: this.audioAlertTone,
});
initOnEvents.forEach(event => {
document.removeEventListener(event, this.onAudioListenEvent, false);
});
this.playAudioEvery30Seconds();
} catch (error) {
// Ignore audio fetch errors
}
};
executeRecurringNotification = () => {
const mineConversation = window.WOOT.$store.getters.getMineChats({
assigneeType: 'me',
status: 'open',
});
const hasUnreadConversation = mineConversation.some(conv => {
return conv.unread_count > 0;
});
const shouldPlayAlert = !this.playAlertOnlyWhenHidden || document.hidden;
if (hasUnreadConversation && shouldPlayAlert) {
window.playAudioAlert();
showBadgeOnFavicon();
}
this.clearSetTimeout();
};
clearSetTimeout = () => {
if (this.recurringNotificationTimer) {
clearTimeout(this.recurringNotificationTimer);
}
this.recurringNotificationTimer = setTimeout(
this.executeRecurringNotification,
NOTIFICATION_TIME
);
};
playAudioEvery30Seconds = () => {
// Audio alert is disabled dismiss the timer
if (this.audioAlertType === 'none') {
return;
}
// If assigned conversation flag is disabled dismiss the timer
if (!this.alertIfUnreadConversationExist) {
return;
}
this.clearSetTimeout();
};
isConversationAssignedToCurrentUser = message => {
const conversationAssigneeId = message?.conversation?.assignee_id;
return conversationAssigneeId === this.currentUserId;
};
isMessageFromCurrentConversation = message => {
return (
window.WOOT.$store.getters.getSelectedChat?.id === message.conversation_id
);
};
isMessageFromCurrentUser = message => {
return message?.sender_id === this.currentUserId;
};
shouldNotifyOnMessage = message => {
if (this.audioAlertType === 'mine') {
return this.isConversationAssignedToCurrentUser(message);
}
return this.audioAlertType === 'all';
};
onNewMessage = message => {
// If the message is sent by the current user or the
// correct notification is not enabled, then dismiss the alert
if (
this.isMessageFromCurrentUser(message) ||
!this.shouldNotifyOnMessage(message)
) {
return;
}
// If the message type is not incoming or private, then dismiss the alert
const { message_type: messageType, private: isPrivate } = message;
if (messageType !== MESSAGE_TYPE.INCOMING && !isPrivate) {
return;
}
// If the user looking at the conversation, then dismiss the alert
if (this.isMessageFromCurrentConversation(message) && !document.hidden) {
return;
}
// If the user has disabled alerts when active on the dashboard, the dismiss the alert
if (this.playAlertOnlyWhenHidden && !document.hidden) {
return;
}
window.playAudioAlert();
showBadgeOnFavicon();
this.playAudioEvery30Seconds();
};
}
export default new DashboardAudioNotificationHelper();
@@ -1,6 +1,6 @@
import AuthAPI from '../api/auth';
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
import { newMessageNotification } from 'shared/helpers/AudioNotificationHelper';
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
@@ -74,7 +74,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onLogout = () => AuthAPI.logout();
onMessageCreated = data => {
newMessageNotification(data);
DashboardAudioNotificationHelper.onNewMessage(data);
this.app.$store.dispatch('addMessage', data);
};
@@ -97,8 +97,9 @@ export const generateConditionOptions = (options, key = 'id') => {
});
};
export const getActionOptions = ({ teams, labels, type }) => {
export const getActionOptions = ({ agents, teams, labels, type }) => {
const actionsMap = {
assign_agent: agents,
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
@@ -178,7 +179,7 @@ export const getDefaultConditions = eventName => {
export const getDefaultActions = () => {
return [
{
action_name: 'assign_team',
action_name: 'assign_agent',
action_params: [],
},
];
@@ -213,7 +214,7 @@ export const isCustomAttribute = (attrs, key) => {
export const generateCustomAttributes = (
conversationAttributes = [],
contactAttribtues = [],
contactAttributes = [],
conversationlabel,
contactlabel
) => {
@@ -228,14 +229,14 @@ export const generateCustomAttributes = (
...conversationAttributes
);
}
if (contactAttribtues.length) {
if (contactAttributes.length) {
customAttributes.push(
{
key: `contact_custom_attribute`,
name: contactlabel,
disabled: true,
},
...contactAttribtues
...contactAttributes
);
}
return customAttributes;
@@ -1,4 +1,5 @@
import AnalyticsHelper from './AnalyticsHelper';
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
@@ -13,6 +14,25 @@ export const initializeAnalyticsEvents = () => {
window.bus.$on(ANALYTICS_RESET, () => {});
};
const initializeAudioAlerts = user => {
// InitializeAudioNotifications
const { ui_settings: uiSettings } = user || {};
const {
always_play_audio_alert: alwaysPlayAudioAlert,
enable_audio_alerts: audioAlertType,
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
notification_tone: audioAlertTone,
} = uiSettings;
DashboardAudioNotificationHelper.setInstanceValues({
currentUserId: user.id,
audioAlertType: audioAlertType || 'none',
audioAlertTone: audioAlertTone || 'ding',
alwaysPlayAudioAlert: alwaysPlayAudioAlert || false,
alertIfUnreadConversationExist: alertIfUnreadConversationExist || false,
});
};
export const initializeChatwootEvents = () => {
window.bus.$on(CHATWOOT_RESET, () => {
if (window.$chatwoot) {
@@ -32,5 +52,7 @@ export const initializeChatwootEvents = () => {
cloudCustomer: 'true',
});
}
initializeAudioAlerts(user);
});
};
@@ -24,8 +24,11 @@
"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.",
"SUBMIT": "تحديث",
"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"
},
"ADD": {
@@ -42,8 +42,8 @@
}
},
"API": {
"SUCCESS_MESSAGE": "تم إضافة سمة مخصصة بنجاح",
"ERROR_MESSAGE": "تعذر إنشاء سمة مخصصة، الرجاء المحاولة مرة أخرى لاحقاً"
"SUCCESS_MESSAGE": "تم إضافة سمة مخصصة بنجاح!",
"ERROR_MESSAGE": "تعذر إنشاء سمة مخصصة، الرجاء المحاولة مرة أخرى لاحقاً."
}
},
"DELETE": {
@@ -2,8 +2,8 @@
"BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
"AGENT_SELECT_LABEL": "اختر وكيل",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
"UNASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من إلغاء تعيين %{conversationCount} %{conversationLabel}؟",
"GO_BACK_LABEL": "العودة للخلف",
"ASSIGN_LABEL": "تكليف",
"YES": "نعم",
@@ -31,10 +31,10 @@
"TEAMS": {
"TEAM_SELECT_LABEL": "اختيار فريق",
"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"
"NO_TEAMS_AVAILABLE": "لا توجد فرق مضافة إلى هذا الحساب حتى الآن.",
"ASSIGN_SELECTED_TEAMS": "تعيين فريق محدد",
"ASSIGN_SUCCESFUL": "تم تعيين الفرق بنجاح",
"ASSIGN_FAILED": "فشل تعيين الفريق، الرجاء المحاولة مرة أخرى"
}
}
}
@@ -57,8 +57,8 @@
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
"SENT": "Sent successfully",
"READ": "Read successfully",
"DELIVERED": "Delivered successfully",
"READ": "تمت القراءة بنجاح",
"DELIVERED": "تم الإرسال بنجاح",
"NO_MESSAGES": "لا توجد رسائل",
"NO_CONTENT": "لم يتم العثور على محتوى",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
@@ -214,7 +214,7 @@
"CONVERSATION_INFO": "معلومات المحادثة",
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
"MACROS": "Macros"
"MACROS": "ماكروس"
}
},
"CONVERSATION_CUSTOM_ATTRIBUTES": {
@@ -1,6 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك"
}
}
@@ -23,7 +23,7 @@
"ERROR": "الرجاء إدخال اسم حساب صحيح"
},
"LANGUAGE": {
"LABEL": "Site language",
"LABEL": "لغة الموقع",
"PLACEHOLDER": "اسم الحساب الخاص بك",
"ERROR": ""
},
@@ -55,7 +55,7 @@
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
"SELECT_ONE": "اختر واحدا",
"SELECT": "Select"
"SELECT": "اختر"
}
},
"NOTIFICATIONS_PAGE": {
@@ -139,6 +139,6 @@
}
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
"LOADING_MESSAGE": "تحميل تطبيق لوحة التحكم..."
}
}
@@ -53,6 +53,10 @@
"ENABLE": "إنشاء محادثات من التغريدات المشار إليها"
}
},
"MICROSOFT": {
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"WEBSITE_CHANNEL": {
"TITLE": "قناة الموقع",
"DESC": "قم بإنشاء قناة تواصل لموقع الويب الخاص بك وابدأ في استقبال الرسائل من عملائك عبر صندوق الدردشة المباشرة.",
@@ -134,7 +138,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء تقديم رقم هاتف صالح يبدأ بإشارة '+' ولا يحتوي على أي مسافات."
},
"API_CALLBACK": {
"TITLE": "عنوان Callback URL",
@@ -185,7 +189,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء تقديم رقم هاتف صالح يبدأ بإشارة '+' ولا يحتوي على أي مسافات."
},
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
"API": {
@@ -214,7 +218,7 @@
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
"ERROR": "الرجاء تقديم رقم هاتف صالح يبدأ بإشارة '+' ولا يحتوي على أي مسافات."
},
"PHONE_NUMBER_ID": {
"LABEL": "رقم الهاتف",
@@ -239,9 +243,9 @@
},
"API_CALLBACK": {
"TITLE": "عنوان Callback URL",
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"SUBTITLE": "يجب عليك تكوين رابط الويب هوك و رمز التحقق في بوابة مطور الفيسبوك مع القيم الموضحة أدناه.",
"WEBHOOK_URL": "رابط Webhook",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
"WEBHOOK_VERIFICATION_TOKEN": "رمز التحقق من Webhook"
},
"SUBMIT_BUTTON": "إنشاء قناة واتساب",
"API": {
@@ -421,7 +425,7 @@
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
"BUSINESS_HOURS": "ساعات العمل",
"WIDGET_BUILDER": "منشئ اللايف شات",
"BOT_CONFIGURATION": "Bot Configuration"
"BOT_CONFIGURATION": "اعدادات البوت"
},
"SETTINGS": "الإعدادات",
"FEATURES": {
@@ -445,8 +449,8 @@
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"LOCK_TO_SINGLE_CONVERSATION": "قفل إلى محادثة واحدة",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "تمكين أو تعطيل محادثات متعددة لنفس جهة الاتصال في هذا البريد الوارد",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
@@ -548,6 +552,10 @@
},
"ENABLE_SSL": "تمكين SSL"
},
"MICROSOFT": {
"TITLE": "Microsoft",
"SUBTITLE": "Reauthorize your MICROSOFT account"
},
"SMTP": {
"TITLE": "SMTP",
"SUBTITLE": "تعيين تفاصيل IMAP الخاصة بك",
@@ -76,6 +76,13 @@
"BODY": "<br/><p>ستتم الآن مزامنة جميع المحادثات الواردة إلى <b><i>محادثة الزبائن</i></b> داخل مكان العمل الخاص بك.</p><p>الرد على موضوع محادثة في <b><i>محادثة العملاء</i></b> قناة الركود سوف تقوم بإنشاء رد على العميل من خلال المحادثة.</p><p>ابدأ الردود ب <b><i>ملاحظة:</i></b> لإنشاء ملاحظات خاصة بدلاً من الردود.</p><p>إذا كان للرد على slack ملف تعريف الوكيل في الدردشة تحت نفس البريد الإلكتروني، فسيتم ربط الردود وفقا لذلك.</p><p>عندما لا يكون للرد ملف شخصي للوكيل المرتبط، ستتم الردود من ملف بوت الشخصي.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"API": {
@@ -1,78 +1,78 @@
{
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
"HEADER": "ماكروس",
"HEADER_BTN_TXT": "إضافة ماكرو جديد",
"HEADER_BTN_TXT_SAVE": "حفظ الماكرو",
"LOADING": "جاري جلب الماكروس",
"SIDEBAR_TXT": "<p><b>الماكروس</b><p>الماكرو هو مجموعة من الإجراءات المحفوظة التي تساعد وكلاء خدمة العملاء على إكمال المهام بسهولة. يمكن للوكلاء تحديد مجموعة من الإجراءات مثل وضع علامة على محادثة مع تسمية، وإرسال نص بريد إلكتروني، وتحديث سمة مخصصة، إلخ. ويمكنهم تنفيذ هذه الإجراءات بنقرة واحدة. وعندما يدير الوكلاء الكلية، يتم تنفيذ الإجراءات بالتسلسل حسب الترتيب الذي تحدده. يحسن الماكرو الإنتاجية ويزيد الاتساق في الإجراءات. </p><p>يمكن أن يكون الماكرو مفيداً بطريقتين. </p><p><b>كوكيل يساعد:</b> إذا قام وكيل بمجموعة من الإجراءات عدة مرات، يمكنهم حفظه كماكلي وتنفيذ جميع الإجراءات معاً باستخدام نقرة واحدة.</p><p><b>كخيار للدخول إلى عضوية الفريق:</b> يجب على كل وكيل إجراء العديد من الشيكات والإجراءات المختلفة خلال كل محادثة. سيكون عضو فريق الدعم الجديد من أونبواردينغ سهلا إذا كان الماكرو المحدد مسبقا متاحا على الحساب. وبدلا من وصف كل خطوة بالتفصيل، يمكن للمدير/الفريق أن يشير إلى الكتلة الكلية المستخدمة في سيناريوهات مختلفة.</p>",
"ERROR": "حدث خطأ ما. الرجاء المحاولة مرة أخرى",
"ORDER_INFO": "سيتم تشغيل الماكرو بالترتيب الذي تضيفه إجراءاتك. يمكنك إعادة ترتيبهم بسحبهم بواسطة المعالج بجانب كل عقدة.",
"ADD": {
"FORM": {
"NAME": {
"LABEL": "Macro name",
"PLACEHOLDER": "Enter a name for your macro",
"ERROR": "Name is required for creating a macro"
"LABEL": "اسم الماكرو",
"PLACEHOLDER": "أدخل اسم الماكرو الخاص بك",
"ERROR": "الاسم مطلوب لإنشاء الماكرو"
},
"ACTIONS": {
"LABEL": "الإجراءات"
}
},
"API": {
"SUCCESS_MESSAGE": "Macro added successfully",
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
"SUCCESS_MESSAGE": "تمت إضافة الماكرو بنجاح",
"ERROR_MESSAGE": "غير قادر على إنشاء الماكرو ، الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"LIST": {
"TABLE_HEADER": [
"الاسم",
"Created by",
"Last updated by",
"Visibility"
"تم إنشاؤها بواسطة",
"آخر تحديث بواسطة",
"الظهور"
],
"404": "No macros found"
"404": "لم يتم العثور على الماكروس"
},
"DELETE": {
"TOOLTIP": "Delete macro",
"TOOLTIP": "حذف الماكرو",
"CONFIRM": {
"MESSAGE": "هل أنت متأكد من الحذف ",
"YES": "نعم، احذف",
"NO": "لا"
},
"API": {
"SUCCESS_MESSAGE": "Macro deleted successfully",
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
"SUCCESS_MESSAGE": "تم حذف الماكرو بنجاح",
"ERROR_MESSAGE": "حدث خطأ أثناء حذف الماكرو. الرجاء المحاولة مرة أخرى في وقت لاحق"
}
},
"EDIT": {
"TOOLTIP": "Edit macro",
"TOOLTIP": "تعديل الماكرو",
"API": {
"SUCCESS_MESSAGE": "Macro updated successfully",
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
"SUCCESS_MESSAGE": "تم تحديث الماكرو بنجاح",
"ERROR_MESSAGE": "تعذر تحديث الماكرو ، الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"EDITOR": {
"START_FLOW": "Start Flow",
"END_FLOW": "End Flow",
"LOADING": "Fetching macro",
"ADD_BTN_TOOLTIP": "Add new action",
"DELETE_BTN_TOOLTIP": "Delete Action",
"START_FLOW": "بداية الإجرائات",
"END_FLOW": "نهاية الإجرائات",
"LOADING": "جاري جلب الماكروس",
"ADD_BTN_TOOLTIP": "إضافة إجراء جديد",
"DELETE_BTN_TOOLTIP": "حذف الإجراء",
"VISIBILITY": {
"LABEL": "Macro Visibility",
"LABEL": "الرؤية الخاصة بالماكرو",
"GLOBAL": {
"LABEL": "Public",
"DESCRIPTION": "This macro is available publicly for all agents in this account."
"LABEL": "عامة",
"DESCRIPTION": "هذا الماكرو متاح بشكل عام لجميع الوكلاء في هذا الحساب."
},
"PERSONAL": {
"LABEL": "Private",
"DESCRIPTION": "This macro will be private to you and not be available to others."
"LABEL": "خاص",
"DESCRIPTION": "هذا الماكرو سيكون خاصا لك ولن يكون متاحا للآخرين."
}
}
},
"EXECUTE": {
"BUTTON_TOOLTIP": "Execute",
"PREVIEW": "Preview Macro",
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
"BUTTON_TOOLTIP": "تنفيذ",
"PREVIEW": "معاينة الماكرو",
"EXECUTED_SUCCESSFULLY": "تم تنفيذ الماكرو بنجاح"
}
}
}
@@ -58,9 +58,20 @@
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "الإشعارات الصوتية",
"NOTE": "تمكين التنبيهات الصوتية في لوحة التحكم للرسائل والمحادثات الجديدة.",
"NONE": "لا شيء",
"ASSIGNED": "المحادثات المسندة",
"ALL_CONVERSATIONS": "كل المحادثات"
"ALERT_TYPE": {
"TITLE": "أحداث التنبيه:",
"NONE": "لا شيء",
"ASSIGNED": "المحادثات المسندة",
"ALL_CONVERSATIONS": "كل المحادثات"
},
"DEFAULT_TONE": {
"TITLE": "نغمة التنبيه:"
},
"CONDITIONS": {
"TITLE": "شروط التنبيه:",
"CONDITION_ONE": "إرسال تنبيهات صوتية فقط إذا كانت نافذة المتصفح غير نشطة",
"CONDITION_TWO": "إرسال تنبيهات كل 30 ثانية حتى يتم قراءة جميع المحادثات المعينة"
}
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "إشعارات البريد الإلكتروني",
@@ -104,8 +115,8 @@
"مشغول",
"غير متصل"
],
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
"SET_AVAILABILITY_SUCCESS": "تم تعيين التوافر بنجاح",
"SET_AVAILABILITY_ERROR": "تعذر تعيين التوافر، الرجاء المحاولة مرة أخرى"
},
"EMAIL": {
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
@@ -136,6 +147,7 @@
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
"PROFILE_SETTINGS": "إعدادات الملف الشخصي",
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
"SUPER_ADMIN_CONSOLE": "وحدة تحكم المدير المتميز",
"LOGOUT": "تسجيل الخروج"
},
"APP_GLOBAL": {
@@ -161,7 +173,7 @@
"UPLOADING": "جاري الرفع..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
"SEE_ON_MAP": "مشاهدة على الخريطة"
},
"FORM_BUBBLE": {
"SUBMIT": "إرسال"
@@ -185,7 +197,7 @@
"CONTACTS": "جهات الاتصال",
"HOME": "الرئيسية",
"AGENTS": "موظف الدعم",
"AGENT_BOTS": "Bots",
"AGENT_BOTS": "البوتات",
"INBOXES": "قنوات التواصل",
"NOTIFICATIONS": "الإشعارات",
"CANNED_RESPONSES": "الردود السريعة",
@@ -196,7 +208,7 @@
"LABELS": "الوسوم",
"CUSTOM_ATTRIBUTES": "سمات مخصصة",
"AUTOMATION": "الأتمتة",
"MACROS": "Macros",
"MACROS": "ماكروس",
"TEAMS": "الفرق",
"BILLING": "الفواتير",
"CUSTOM_VIEWS_FOLDER": "المجلدات",
@@ -229,8 +241,8 @@
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
},
"SET_AUTO_OFFLINE": {
"TEXT": "Mark offline automatically",
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
"TEXT": "وضع علامة غير متصل تلقائيا",
"INFO_TEXT": "السماح للنظام بوضع علامة غير متصل أوتوماتيكياً عندما لا تستخدم التطبيق أو لوحة التحكم."
},
"DOCS": "قراءة المستندات"
},
@@ -270,6 +282,7 @@
}
},
"KEYBOARD_SHORTCUTS": {
"TOGGLE_MODAL": "عرض جميع الاختصارات",
"TITLE": {
"OPEN_CONVERSATION": "فتح المحادثة",
"RESOLVE_AND_NEXT": "حل وانتقل إلى التالي",
@@ -2,7 +2,7 @@
"REGISTER": {
"TRY_WOOT": "تسجيل حساب",
"TITLE": "تسجيل",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_HEADER": "إن كل ما يلزم هو خطوة واحدة للمضي قدما",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>",
"COMPANY_NAME": {
@@ -24,8 +24,11 @@
"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.",
"SUBMIT": "Обновяване",
"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"
},
"ADD": {
@@ -42,8 +42,8 @@
}
},
"API": {
"SUCCESS_MESSAGE": "Custom Attribute added successfully",
"ERROR_MESSAGE": "Could not able to create a custom attribute, Please try again later"
"SUCCESS_MESSAGE": "Custom Attribute added successfully!",
"ERROR_MESSAGE": "Could not create a Custom Attribute. Please try again later."
}
},
"DELETE": {
@@ -53,6 +53,10 @@
"ENABLE": "Create conversations from mentioned Tweets"
}
},
"MICROSOFT": {
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"WEBSITE_CHANNEL": {
"TITLE": "Website channel",
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",
@@ -548,6 +552,10 @@
},
"ENABLE_SSL": "Enable SSL"
},
"MICROSOFT": {
"TITLE": "Microsoft",
"SUBTITLE": "Reauthorize your MICROSOFT account"
},
"SMTP": {
"TITLE": "SMTP",
"SUBTITLE": "Set your SMTP details",
@@ -76,6 +76,13 @@
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": {
"BUTTON_TEXT": "Изтрий",
"API": {
@@ -58,9 +58,20 @@
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Notifications",
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
"NONE": "None",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
"ALERT_TYPE": {
"TITLE": "Alert events:",
"NONE": "Нито един",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
},
"DEFAULT_TONE": {
"TITLE": "Alert tone:"
},
"CONDITIONS": {
"TITLE": "Alert conditions:",
"CONDITION_ONE": "Send audio alerts only if the browser window is not active",
"CONDITION_TWO": "Send alerts every 30s until all the assigned conversations are read"
}
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "Email Notifications",
@@ -136,6 +147,7 @@
"SELECTOR_SUBTITLE": "Select an account from the following list",
"PROFILE_SETTINGS": "Profile Settings",
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "Logout"
},
"APP_GLOBAL": {
@@ -270,6 +282,7 @@
}
},
"KEYBOARD_SHORTCUTS": {
"TOGGLE_MODAL": "View all shortcuts",
"TITLE": {
"OPEN_CONVERSATION": "Open conversation",
"RESOLVE_AND_NEXT": "Resolve and move to next",
@@ -24,8 +24,11 @@
"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.",
"SUBMIT": "Actualitza",
"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"
},
"ADD": {
@@ -42,8 +42,8 @@
}
},
"API": {
"SUCCESS_MESSAGE": "Custom Attribute added successfully",
"ERROR_MESSAGE": "Could not able to create a custom attribute, Please try again later"
"SUCCESS_MESSAGE": "Custom Attribute added successfully!",
"ERROR_MESSAGE": "Could not create a Custom Attribute. Please try again later."
}
},
"DELETE": {
@@ -53,6 +53,10 @@
"ENABLE": "Create conversations from mentioned Tweets"
}
},
"MICROSOFT": {
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"WEBSITE_CHANNEL": {
"TITLE": "Canal Web",
"DESC": "Crea un canal per al vostre lloc web i comença a donar suport als vostres clients mitjançant el teu widget del lloc web.",
@@ -548,6 +552,10 @@
},
"ENABLE_SSL": "Enable SSL"
},
"MICROSOFT": {
"TITLE": "Microsoft",
"SUBTITLE": "Reauthorize your MICROSOFT account"
},
"SMTP": {
"TITLE": "SMTP",
"SUBTITLE": "Set your SMTP details",
@@ -76,6 +76,13 @@
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": {
"BUTTON_TEXT": "Suprimeix",
"API": {
@@ -58,9 +58,20 @@
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Notifications",
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
"NONE": "None",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
"ALERT_TYPE": {
"TITLE": "Alert events:",
"NONE": "Ningú",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
},
"DEFAULT_TONE": {
"TITLE": "Alert tone:"
},
"CONDITIONS": {
"TITLE": "Alert conditions:",
"CONDITION_ONE": "Send audio alerts only if the browser window is not active",
"CONDITION_TWO": "Send alerts every 30s until all the assigned conversations are read"
}
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "Notificacions per correu electrònic",
@@ -136,6 +147,7 @@
"SELECTOR_SUBTITLE": "Selecciona un compte de la llista següent",
"PROFILE_SETTINGS": "Configuració del Perfil",
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "Sortir"
},
"APP_GLOBAL": {
@@ -270,6 +282,7 @@
}
},
"KEYBOARD_SHORTCUTS": {
"TOGGLE_MODAL": "View all shortcuts",
"TITLE": {
"OPEN_CONVERSATION": "Open conversation",
"RESOLVE_AND_NEXT": "Resolve and move to next",
@@ -24,8 +24,11 @@
"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.",
"SUBMIT": "Aktualizovat",
"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"
},
"ADD": {
@@ -42,8 +42,8 @@
}
},
"API": {
"SUCCESS_MESSAGE": "Custom Attribute added successfully",
"ERROR_MESSAGE": "Could not able to create a custom attribute, Please try again later"
"SUCCESS_MESSAGE": "Custom Attribute added successfully!",
"ERROR_MESSAGE": "Could not create a Custom Attribute. Please try again later."
}
},
"DELETE": {
@@ -53,6 +53,10 @@
"ENABLE": "Create conversations from mentioned Tweets"
}
},
"MICROSOFT": {
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"WEBSITE_CHANNEL": {
"TITLE": "Kanál webové stránky",
"DESC": "Vytvořte si kanál pro vaše webové stránky a začněte podporovat své zákazníky prostřednictvím našeho widgetu.",
@@ -548,6 +552,10 @@
},
"ENABLE_SSL": "Enable SSL"
},
"MICROSOFT": {
"TITLE": "Microsoft",
"SUBTITLE": "Reauthorize your MICROSOFT account"
},
"SMTP": {
"TITLE": "SMTP",
"SUBTITLE": "Set your SMTP details",
@@ -76,6 +76,13 @@
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": {
"BUTTON_TEXT": "Vymazat",
"API": {
@@ -58,9 +58,20 @@
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Notifications",
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
"NONE": "Nic",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
"ALERT_TYPE": {
"TITLE": "Alert events:",
"NONE": "Nic",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
},
"DEFAULT_TONE": {
"TITLE": "Alert tone:"
},
"CONDITIONS": {
"TITLE": "Alert conditions:",
"CONDITION_ONE": "Send audio alerts only if the browser window is not active",
"CONDITION_TWO": "Send alerts every 30s until all the assigned conversations are read"
}
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "E-mailová oznámení",
@@ -136,6 +147,7 @@
"SELECTOR_SUBTITLE": "Vyberte účet z následujícího seznamu",
"PROFILE_SETTINGS": "Nastavení profilu",
"KEYBOARD_SHORTCUTS": "Keyboard Shortcuts",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "Odhlásit se"
},
"APP_GLOBAL": {
@@ -270,6 +282,7 @@
}
},
"KEYBOARD_SHORTCUTS": {
"TOGGLE_MODAL": "View all shortcuts",
"TITLE": {
"OPEN_CONVERSATION": "Open conversation",
"RESOLVE_AND_NEXT": "Resolve and move to next",
@@ -1,32 +1,35 @@
{
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...",
"LOADING_EDITOR": "Indlæser Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
"LABEL": "Bot navn",
"PLACEHOLDER": "Giv din bot et navn",
"ERROR": "Bot navn er påkrævet"
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"PLACEHOLDER": "What does this bot do?"
"LABEL": "Bot beskrivelse",
"PLACEHOLDER": "Hvad gør denne bot?"
},
"BOT_CONFIG": {
"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"
"SUBMIT": "Bekræft og gem"
},
"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.",
"SUBMIT": "Opdater",
"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",
"SELECT_PLACEHOLDER": "Select Bot"
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Vælg bot"
},
"ADD": {
"TITLE": "Configure new bot",
@@ -43,7 +46,7 @@
},
"DELETE": {
"BUTTON_TEXT": "Slet",
"TITLE": "Delete Bot",
"TITLE": "Slet bot",
"SUBMIT": "Slet",
"CANCEL_BUTTON_TEXT": "Annuller",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
@@ -42,8 +42,8 @@
}
},
"API": {
"SUCCESS_MESSAGE": "Brugerdefineret attribut blev tilføjet",
"ERROR_MESSAGE": "Kunne ikke oprette en brugerdefineret attribut. Prøv igen senere"
"SUCCESS_MESSAGE": "Brugerdefineret attribut blev tilføjet!",
"ERROR_MESSAGE": "Could not create a Custom Attribute. Please try again later."
}
},
"DELETE": {
@@ -32,7 +32,7 @@
"TEAM_SELECT_LABEL": "Vælg Team",
"NONE": "Ingen",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SELECTED_TEAMS": "Tildel det valgte team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
}
@@ -42,8 +42,8 @@
"RATING_TITLE": "Bedømmelse",
"FEEDBACK_TITLE": "Tilbagemelding",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
"SHOW_LABELS": "Vis etiketter",
"HIDE_LABELS": "Skjul etiketter"
},
"HEADER": {
"RESOLVE_ACTION": "Løs",
@@ -68,7 +68,7 @@
"CARD_CONTEXT_MENU": {
"PENDING": "Markér som afventende",
"RESOLVED": "Marker som løst",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_UNREAD": "Marker som ulæst",
"REOPEN": "Genåbn samtale",
"SNOOZE": {
"TITLE": "Udsæt",
@@ -1,6 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
"PLACEHOLDER": "Søg emojis",
"NOT_FOUND": "Ingen emoji matcher din søgning"
}
}
@@ -55,7 +55,7 @@
"ENTER_TO_SELECT": "Tryk enter for at vælge",
"ENTER_TO_REMOVE": "Tryk enter for at fjerne",
"SELECT_ONE": "Vælg en",
"SELECT": "Select"
"SELECT": "Vælg"
}
},
"NOTIFICATIONS_PAGE": {
@@ -139,6 +139,6 @@
}
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
"LOADING_MESSAGE": "Indlæser Dashboard App..."
}
}
@@ -217,14 +217,14 @@
"DOMAIN": {
"LABEL": "Tilpasset Domæne",
"PLACEHOLDER": "Portal brugerdefineret domæne",
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
"ERROR": "Enter a valid domain URL"
"HELP_TEXT": "Tilføj kun hvis du vil bruge et brugerdefineret domæne, til dine portaler. Eksempelvis https://example.com",
"ERROR": "Indtast et gyldigt domæne URL"
},
"HOME_PAGE_LINK": {
"LABEL": "Link Til Hjemmeside",
"PLACEHOLDER": "Link til portalens hjemmeside",
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
"ERROR": "Enter a valid home page URL"
"ERROR": "Indtast en gyldig URL til startsiden"
},
"THEME_COLOR": {
"LABEL": "Portal tema farve",
@@ -53,6 +53,10 @@
"ENABLE": "Opret samtaler fra nævnte Tweets"
}
},
"MICROSOFT": {
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"WEBSITE_CHANNEL": {
"TITLE": "Hjemmesidekanal",
"DESC": "Opret en kanal til din hjemmeside og begynde at supporte dine kunder via vores hjemmeside widget.",
@@ -239,9 +243,9 @@
},
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
"SUBTITLE": "Du skal konfigurere dit webhook URL og verifikations token, i Facebook Developer portal med værdierne vist nedenfor.",
"WEBHOOK_URL": "Webhook URL",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
"WEBHOOK_VERIFICATION_TOKEN": "Webhook verifikations token"
},
"SUBMIT_BUTTON": "Opret WhatsApp Kanal",
"API": {
@@ -421,7 +425,7 @@
"PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Forretningstider",
"WIDGET_BUILDER": "Widget Builder",
"BOT_CONFIGURATION": "Bot Configuration"
"BOT_CONFIGURATION": "Bot konfiguration"
},
"SETTINGS": "Indstillinger",
"FEATURES": {
@@ -548,6 +552,10 @@
},
"ENABLE_SSL": "Aktiver SSL"
},
"MICROSOFT": {
"TITLE": "Microsoft",
"SUBTITLE": "Reauthorize your MICROSOFT account"
},
"SMTP": {
"TITLE": "Smtp",
"SUBTITLE": "Indstil dine IMAP-oplysninger",
@@ -76,6 +76,13 @@
"BODY": "<br/><p>Chatwoot vil nu synkronisere alle indgående samtaler ind i <b><i>kundesamtaler</i></b> kanalen i din slack arbejdsplads.</p><p>Svar på en samtaletråd i <b><i>kunde-samtaler</i></b> slack kanal vil skabe et svar tilbage til kunden gennem chatwoot.</p><p>Start svarene med <b><i>note:</i></b> for at oprette private noter i stedet for svar.</p><p>Hvis replikatoren på slack har en agentprofil i chatwoot under samme e-mail, vil svarene blive tilknyttet i overensstemmelse hermed.</p><p>Når replikatoren ikke har en tilknyttet agentprofil, vil svarene blive fremsat fra bot-profilen.</p>"
}
},
"DYTE": {
"CLICK_HERE_TO_JOIN": "Click here to join",
"LEAVE_THE_ROOM": "Leave the room",
"START_VIDEO_CALL_HELP_TEXT": "Start a new video call with the customer",
"JOIN_ERROR": "There was an error joining the call, please try again",
"CREATE_ERROR": "There was an error creating a meeting link, please try again"
},
"DELETE": {
"BUTTON_TEXT": "Slet",
"API": {
@@ -2,7 +2,7 @@
"MACROS": {
"HEADER": "Macros",
"HEADER_BTN_TXT": "Add a new macro",
"HEADER_BTN_TXT_SAVE": "Save macro",
"HEADER_BTN_TXT_SAVE": "Gem macro",
"LOADING": "Fetching macros",
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
"ERROR": "Something went wrong. Please try again",
@@ -26,9 +26,9 @@
"LIST": {
"TABLE_HEADER": [
"Navn",
"Created by",
"Oprettet af",
"Last updated by",
"Visibility"
"Synlighed"
],
"404": "No macros found"
},
@@ -64,7 +64,7 @@
"DESCRIPTION": "This macro is available publicly for all agents in this account."
},
"PERSONAL": {
"LABEL": "Private",
"LABEL": "Privat",
"DESCRIPTION": "This macro will be private to you and not be available to others."
}
}
@@ -20,9 +20,9 @@
"NOTE": "Din e-mailadresse er din identitet og bruges til at logge ind."
},
"SEND_MESSAGE": {
"TITLE": "Hotkey to send messages",
"TITLE": "Genvejstast til at sende beskeder",
"NOTE": "You can select a hotkey (either Enter or Cmd/Ctrl+Enter) based on your preference of writing.",
"UPDATE_SUCCESS": "Your settings have been updated successfully",
"UPDATE_SUCCESS": "Dine indstillinger er blevet opdateret",
"CARD": {
"ENTER_KEY": {
"HEADING": "Enter (↵)",
@@ -58,9 +58,20 @@
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Lyd Notifikationer",
"NOTE": "Aktivér lydmeddelelser i dashboard for nye beskeder og samtaler.",
"NONE": "Ingen",
"ASSIGNED": "Tildelte Samtaler",
"ALL_CONVERSATIONS": "Alle Samtaler"
"ALERT_TYPE": {
"TITLE": "Alert events:",
"NONE": "Ingen",
"ASSIGNED": "Tildelte Samtaler",
"ALL_CONVERSATIONS": "Alle Samtaler"
},
"DEFAULT_TONE": {
"TITLE": "Alert tone:"
},
"CONDITIONS": {
"TITLE": "Alert conditions:",
"CONDITION_ONE": "Send audio alerts only if the browser window is not active",
"CONDITION_TWO": "Send alerts every 30s until all the assigned conversations are read"
}
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "E-Mail Notifikationer",
@@ -136,6 +147,7 @@
"SELECTOR_SUBTITLE": "Vælg en konto fra følgende liste",
"PROFILE_SETTINGS": "Profilindstillinger",
"KEYBOARD_SHORTCUTS": "Tastaturgenveje",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "Log Ud"
},
"APP_GLOBAL": {
@@ -161,7 +173,7 @@
"UPLOADING": "Uploader..."
},
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
"SEE_ON_MAP": "Se på kort"
},
"FORM_BUBBLE": {
"SUBMIT": "Send"
@@ -270,6 +282,7 @@
}
},
"KEYBOARD_SHORTCUTS": {
"TOGGLE_MODAL": "View all shortcuts",
"TITLE": {
"OPEN_CONVERSATION": "Åbn samtale",
"RESOLVE_AND_NEXT": "Løs og flyt til næste",
@@ -2,13 +2,13 @@
"REGISTER": {
"TRY_WOOT": "Registrer en konto",
"TITLE": "Registrer",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TESTIMONIAL_HEADER": "Alt, hvad der skal til, er blot et skridt for at komme videre",
"TESTIMONIAL_CONTENT": "Du er et skridt fra at engagere dine kunder, fastholde dem og finde nye kunder.",
"TERMS_ACCEPT": "Ved at tilmelde dig, accepterer du vores <a href=\"https://www.chatwoot.com/terms\">T & C</a> og <a href=\"https://www.chatwoot.com/privacy-policy\">Privatlivspolitik</a>",
"COMPANY_NAME": {
"LABEL": "Company name",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",
"ERROR": "Company name is too short"
"LABEL": "Firmanavn",
"PLACEHOLDER": "Indtast dit firmanavn, fx: Wayne Enterprises",
"ERROR": "Firmanavn er for kort"
},
"FULL_NAME": {
"LABEL": "Fulde navn",
@@ -18,7 +18,7 @@
"EMAIL": {
"LABEL": "Arbejde e-mail",
"PLACEHOLDER": "Indtast din arbejdsmailadresse fx: bruce@wayne.enterprises",
"ERROR": "Please enter a valid work email address"
"ERROR": "Indtast venligst en gyldig arbejdsmail"
},
"PASSWORD": {
"LABEL": "Adgangskode",
@@ -24,8 +24,11 @@
"TITLE": "Agenten-Bot auswählen",
"DESC": "Sie können einen Agenten-Bot aus der Liste in diesen Posteingang setzen. Der Bot kann die Unterhaltung anfangs bearbeiten und bei Bedarf an einen Agenten übertragen.",
"SUBMIT": "Aktualisieren",
"DISCONNECT": "Disconnect Bot",
"SUCCESS_MESSAGE": "Agenten-Bot erfolgreich aktualisiert",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot",
"ERROR_MESSAGE": "Konnte den Agenten-Bot nicht aktualisieren, bitte versuchen Sie es später erneut",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Bot auswählen"
},
"ADD": {
@@ -37,29 +40,29 @@
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
"404": "Keine Bots gefunden, Sie können einen Bot erstellen, indem Sie auf den 'Neuen Bot konfigurieren' Button klicken ↗",
"LOADING": "Bots werden geladen...",
"TYPE": "Bot-Typ"
},
"DELETE": {
"BUTTON_TEXT": "Löschen",
"TITLE": "Delete Bot",
"TITLE": "Bot löschen",
"SUBMIT": "Löschen",
"CANCEL_BUTTON_TEXT": "Stornieren",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"DESCRIPTION": "Sind Sie sicher, dass Sie diesen Bot löschen wollen? Diese Aktion kann nicht rückgängig gemacht werden",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"SUCCESS_MESSAGE": "Bot erfolgreich gelöscht",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "Bearbeiten",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"LOADING": "Bots werden geladen...",
"TITLE": "Bot bearbeiten",
"CANCEL_BUTTON_TEXT": "Stornieren",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
"SUCCESS_MESSAGE": "Bot erfolgreich aktualisiert",
"ERROR_MESSAGE": "Bot konnte nicht aktualisiert werden, bitte versuchen Sie es später erneut"
}
},
"TYPES": {

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