Merge branch 'release/1.21.0'

This commit is contained in:
Sojan
2021-10-16 00:13:06 +05:30
703 changed files with 22272 additions and 2715 deletions

3
.bundler-audit.yml Normal file
View File

@@ -0,0 +1,3 @@
---
ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)

View File

@@ -14,6 +14,10 @@ plugins:
checks: checks:
similar-code: similar-code:
enabled: false enabled: false
method-count:
enabled: true
config:
threshold: 25
exclude_patterns: exclude_patterns:
- "spec/" - "spec/"
- "**/specs/" - "**/specs/"

View File

@@ -100,6 +100,9 @@ FB_VERIFY_TOKEN=
FB_APP_SECRET= FB_APP_SECRET=
FB_APP_ID= FB_APP_ID=
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
IG_VERIFY_TOKEN=
# Twitter # Twitter
# documentation: https://www.chatwoot.com/docs/twitter-app-setup # documentation: https://www.chatwoot.com/docs/twitter-app-setup
TWITTER_APP_ID= TWITTER_APP_ID=
@@ -113,7 +116,7 @@ SLACK_CLIENT_SECRET=
### Change this env variable only if you are using a custom build mobile app ### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables ## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app IOS_APP_ID=L7YLMN4634.com.chatwoot.app
ANDROID_BUNDLE_ID=com.chatwoot.app ANDROID_BUNDLE_ID=com.chatwoot.app
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section) # https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
@@ -166,7 +169,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
## Rack Attack configuration ## Rack Attack configuration
## To prevent and throttle abusive requests ## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=false # ENABLE_RACK_ATTACK=true
## Running chatwoot as an API only server ## Running chatwoot as an API only server

11
Gemfile
View File

@@ -56,7 +56,6 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'rack-timeout'
gem 'webpacker', '~> 5.x' gem 'webpacker', '~> 5.x'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@@ -122,6 +121,11 @@ gem 'hairtrigger'
gem 'procore-sift' gem 'procore-sift'
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
end
group :development do group :development do
gem 'annotate' gem 'annotate'
gem 'bullet' gem 'bullet'
@@ -143,6 +147,11 @@ group :test do
end end
group :development, :test do group :development, :test do
# TODO: is this needed ?
# errors thrown by devise password gem
gem 'flay'
gem 'rspec'
# for error thrown by devise password gem
gem 'active_record_query_trace' gem 'active_record_query_trace'
gem 'bundle-audit', require: false gem 'bundle-audit', require: false
gem 'byebug', platform: :mri gem 'byebug', platform: :mri

View File

@@ -90,21 +90,21 @@ GEM
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (6.2.4) attr_extras (6.2.4)
aws-eventstream (1.1.1) aws-eventstream (1.2.0)
aws-partitions (1.482.0) aws-partitions (1.513.0)
aws-sdk-core (3.119.0) aws-sdk-core (3.121.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.46.0) aws-sdk-kms (1.49.0)
aws-sdk-core (~> 3, >= 3.119.0) aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.98.0) aws-sdk-s3 (1.103.0)
aws-sdk-core (~> 3, >= 3.119.0) aws-sdk-core (~> 3, >= 3.120.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.4)
aws-sigv4 (1.2.4) aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.1) azure-storage-blob (2.0.1)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -119,28 +119,28 @@ GEM
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
bcrypt (3.1.16) bcrypt (3.1.16)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.7.7) bootsnap (1.9.1)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (5.1.1) brakeman (5.1.1)
browser (5.3.1) browser (5.3.1)
builder (3.2.4) builder (3.2.4)
bullet (6.1.4) bullet (6.1.5)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
bundler-audit bundler-audit
bundler-audit (0.8.0) bundler-audit (0.9.0.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.22.0) commonmarker (0.23.2)
concurrent-ruby (1.1.9) concurrent-ruby (1.1.9)
connection_pool (2.2.5) connection_pool (2.2.5)
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
cypress-on-rails (1.10.1) cypress-on-rails (1.11.0)
rack rack
database_cleaner (2.0.1) database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (~> 2.0.0)
@@ -150,7 +150,7 @@ GEM
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7) datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1) momentjs-rails (>= 2.8.1)
ddtrace (0.51.1) ddtrace (0.53.0)
ffi (~> 1.0) ffi (~> 1.0)
msgpack msgpack
declarative (0.0.20) declarative (0.0.20)
@@ -174,12 +174,13 @@ GEM
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
dotenv (= 2.7.6) dotenv (= 2.7.6)
railties (>= 3.2) railties (>= 3.2)
down (5.2.3) down (5.2.4)
addressable (~> 2.8) addressable (~> 2.8)
ecma-re-validator (0.3.0) ecma-re-validator (0.3.0)
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
erubi (1.10.0) erubi (1.10.0)
et-orbi (1.2.4) erubis (2.7.0)
et-orbi (1.2.5)
tzinfo tzinfo
execjs (2.8.1) execjs (2.8.1)
facebook-messenger (2.0.1) facebook-messenger (2.0.1)
@@ -190,7 +191,7 @@ GEM
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (2.18.0) faker (2.19.0)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.0.1) faraday (1.0.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@@ -198,10 +199,15 @@ GEM
faraday (~> 1.0) faraday (~> 1.0)
fcm (1.0.3) fcm (1.0.3)
faraday (~> 1) faraday (~> 1)
ffi (1.15.3) ffi (1.15.4)
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
flay (2.12.1)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
foreman (0.87.2) foreman (0.87.2)
fugit (1.5.0) fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8) et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.3.4) gapic-common (0.3.4)
@@ -210,7 +216,7 @@ GEM
googleapis-common-protos-types (>= 1.0.4, < 2.0) googleapis-common-protos-types (>= 1.0.4, < 2.0)
googleauth (~> 0.9) googleauth (~> 0.9)
grpc (~> 1.25) grpc (~> 1.25)
geocoder (1.6.7) geocoder (1.7.0)
gli (2.20.1) gli (2.20.1)
globalid (0.5.2) globalid (0.5.2)
activesupport (>= 5.0) activesupport (>= 5.0)
@@ -223,9 +229,9 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.6.0) google-apis-iamcredentials_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.6.0) google-apis-storage_v1 (0.8.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
@@ -238,7 +244,7 @@ GEM
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0) google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.1.0) google-cloud-errors (1.2.0)
google-cloud-storage (1.34.1) google-cloud-storage (1.34.1)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@@ -247,28 +253,32 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-protobuf (3.17.3-universal-darwin) google-protobuf (3.18.1)
google-protobuf (3.17.3-x86_64-linux) google-protobuf (3.18.1-universal-darwin)
googleapis-common-protos (1.3.11) google-protobuf (3.18.1-x86_64-linux)
googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleapis-common-protos-types (>= 1.0.6, < 2.0) googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27) grpc (~> 1.27)
googleapis-common-protos-types (1.1.0) googleapis-common-protos-types (1.2.0)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleauth (0.17.0) googleauth (0.17.1)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.14) signet (~> 0.15)
groupdate (5.2.2) groupdate (5.2.2)
activesupport (>= 5) activesupport (>= 5)
grpc (1.38.0-universal-darwin) grpc (1.41.0)
google-protobuf (~> 3.15) google-protobuf (~> 3.17)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.38.0-x86_64-linux) grpc (1.41.0-universal-darwin)
google-protobuf (~> 3.15) google-protobuf (~> 3.17)
googleapis-common-protos-types (~> 1.0)
grpc (1.41.0-x86_64-linux)
google-protobuf (~> 3.17)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1) haikunator (1.1.1)
hairtrigger (0.2.24) hairtrigger (0.2.24)
@@ -282,7 +292,7 @@ GEM
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.4) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
httparty (0.18.1) httparty (0.20.0)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
@@ -306,7 +316,7 @@ GEM
hana (~> 1.3) hana (~> 1.3)
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
uri_template (~> 0.7) uri_template (~> 0.7)
jwt (2.2.3) jwt (2.3.0)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1) kaminari-actionview (= 1.2.1)
@@ -327,9 +337,9 @@ GEM
addressable (~> 2.7) addressable (~> 2.7)
letter_opener (1.7.0) letter_opener (1.7.0)
launchy (~> 2.2) launchy (~> 2.2)
line-bot-api (1.21.0) line-bot-api (1.22.0)
liquid (5.0.1) liquid (5.1.0)
listen (3.6.0) listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.12.0) loofah (2.12.0)
@@ -337,17 +347,18 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
marcel (1.0.1) marcel (1.0.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memoist (0.16.2) memoist (0.16.2)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.3.1) mime-types (3.3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2021.0704) mime-types-data (3.2021.0901)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.1) mini_mime (1.1.2)
mini_portile2 (2.5.3)
minitest (5.14.4) minitest (5.14.4)
mock_redis (0.28.0) mock_redis (0.29.0)
ruby2_keywords ruby2_keywords
momentjs-rails (2.20.1) momentjs-rails (2.20.1)
railties (>= 3.1) railties (>= 3.1)
@@ -358,8 +369,11 @@ GEM
net-http-persistent (4.0.1) net-http-persistent (4.0.1)
connection_pool (~> 2.2) connection_pool (~> 2.2)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (7.2.0) newrelic_rpm (8.0.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.7-arm64-darwin) nokogiri (1.11.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.11.7-x86_64-darwin) nokogiri (1.11.7-x86_64-darwin)
@@ -369,9 +383,10 @@ GEM
oauth (0.5.6) oauth (0.5.6)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.1) os (1.1.1)
parallel (1.20.1) parallel (1.21.0)
parser (3.0.2.0) parser (3.0.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
path_expander (1.1.0)
pg (1.2.3) pg (1.2.3)
procore-sift (0.16.0) procore-sift (0.16.0)
rails (> 4.2.0) rails (> 4.2.0)
@@ -381,9 +396,9 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.4.0) puma (5.5.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.5.2) racc (1.5.2)
@@ -415,7 +430,7 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.1) rails-html-sanitizer (1.4.2)
loofah (~> 2.3) loofah (~> 2.3)
railties (6.1.4.1) railties (6.1.4.1)
actionpack (= 6.1.4.1) actionpack (= 6.1.4.1)
@@ -446,6 +461,10 @@ GEM
netrc (~> 0.8) netrc (~> 0.8)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5) rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1) rspec-core (3.10.1)
rspec-support (~> 3.10.0) rspec-support (~> 3.10.0)
rspec-expectations (3.10.1) rspec-expectations (3.10.1)
@@ -454,7 +473,7 @@ GEM
rspec-mocks (3.10.2) rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0) rspec-support (~> 3.10.0)
rspec-rails (5.0.1) rspec-rails (5.0.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
@@ -463,35 +482,34 @@ GEM
rspec-mocks (~> 3.10) rspec-mocks (~> 3.10)
rspec-support (~> 3.10) rspec-support (~> 3.10)
rspec-support (3.10.2) rspec-support (3.10.2)
rubocop (1.18.4) rubocop (1.22.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml rexml
rubocop-ast (>= 1.8.0, < 2.0) rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.8.0) rubocop-ast (1.12.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-performance (1.11.4) rubocop-performance (1.11.5)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.11.3) rubocop-rails (2.12.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.4.0) rubocop-rspec (2.5.0)
rubocop (~> 1.0) rubocop (~> 1.19)
rubocop-ast (>= 1.1.0)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-vips (2.1.2) ruby-vips (2.1.3)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
ruby2ruby (2.4.4) ruby2ruby (2.4.4)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_parser (3.16.0) ruby_parser (3.17.0)
sexp_processor (~> 4.15, >= 4.15.1) sexp_processor (~> 4.15, >= 4.15.1)
sassc (2.4.0) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
@@ -501,38 +519,38 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
scout_apm (4.1.1) scout_apm (4.1.2)
parser parser
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (4.6.4) sentry-rails (4.7.3)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby-core (~> 4.6.0) sentry-ruby-core (~> 4.7.0)
sentry-ruby (4.6.4) sentry-ruby (4.7.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
faraday (>= 1.0) faraday (>= 1.0)
sentry-ruby-core (= 4.6.4) sentry-ruby-core (= 4.7.3)
sentry-ruby-core (4.6.4) sentry-ruby-core (4.7.3)
concurrent-ruby concurrent-ruby
faraday faraday
sentry-sidekiq (4.6.4) sentry-sidekiq (4.7.3)
sentry-ruby-core (~> 4.6.0) sentry-ruby-core (~> 4.7.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.15.3) sexp_processor (4.15.3)
shoulda-matchers (5.0.0) shoulda-matchers (5.0.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.2.1) sidekiq (6.2.2)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
sidekiq-cron (1.2.0) sidekiq-cron (1.2.0)
fugit (~> 1.1) fugit (~> 1.1)
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
signet (0.15.0) signet (0.16.0)
addressable (~> 2.3) addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
@@ -575,15 +593,15 @@ GEM
oauth oauth
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.1) tzinfo-data (1.2021.3)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
uglifier (4.2.0) uglifier (4.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.7) unf_ext (0.0.8)
unicode-display_width (2.0.0) unicode-display_width (2.1.0)
uniform_notifier (1.14.2) uniform_notifier (1.14.2)
uri_template (0.7.0) uri_template (0.7.0)
valid_email2 (4.0.0) valid_email2 (4.0.0)
@@ -596,11 +614,11 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.13.0) webmock (3.14.0)
addressable (>= 2.3.6) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.0) webpacker (5.4.3)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
@@ -617,6 +635,7 @@ GEM
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
ruby
x86_64-darwin-18 x86_64-darwin-18
x86_64-darwin-20 x86_64-darwin-20
x86_64-darwin-21 x86_64-darwin-21
@@ -652,6 +671,7 @@ DEPENDENCIES
faker faker
fcm fcm
flag_shih_tzu flag_shih_tzu
flay
foreman foreman
geocoder geocoder
google-cloud-dialogflow google-cloud-dialogflow
@@ -687,6 +707,7 @@ DEPENDENCIES
redis-namespace redis-namespace
responders responders
rest-client rest-client
rspec
rspec-rails (~> 5.0.0) rspec-rails (~> 5.0.0)
rubocop rubocop
rubocop-performance rubocop-performance

View File

@@ -42,7 +42,7 @@ class ContactMergeAction
end end
def merge_and_remove_mergee_contact def merge_and_remove_mergee_contact
mergable_attribute_keys = %w[identifier name email phone_number custom_attributes] mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes]
base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank
mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank

View File

@@ -4,10 +4,11 @@
# based on this we are showing "not sent from chatwoot" message in frontend # based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages. # Hence there is no need to set user_id in message for outgoing echo messages.
class Messages::Facebook::MessageBuilder class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
attr_reader :response attr_reader :response
def initialize(response, inbox, outgoing_echo: false) def initialize(response, inbox, outgoing_echo: false)
super()
@response = response @response = response
@inbox = inbox @inbox = inbox
@outgoing_echo = outgoing_echo @outgoing_echo = outgoing_echo
@@ -47,30 +48,12 @@ class Messages::Facebook::MessageBuilder
def build_message def build_message
@message = conversation.messages.create!(message_params) @message = conversation.messages.create!(message_params)
@attachments.each do |attachment| @attachments.each do |attachment|
process_attachment(attachment) process_attachment(attachment)
end end
end end
def process_attachment(attachment)
return if attachment['type'].to_sym == :template
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
def attach_file(attachment, file_url)
attachment_file = Down.download(
file_url
)
attachment.file.attach(
io: attachment_file,
filename: attachment_file.original_filename,
content_type: attachment_file.content_type
)
end
def ensure_contact_avatar def ensure_contact_avatar
return if contact_params[:remote_avatar_url].blank? return if contact_params[:remote_avatar_url].blank?
return if @contact.avatar.attached? return if @contact.avatar.attached?
@@ -89,28 +72,6 @@ class Messages::Facebook::MessageBuilder
)) ))
end end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
end
params
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
def location_params(attachment) def location_params(attachment)
lat = attachment['payload']['coordinates']['lat'] lat = attachment['payload']['coordinates']['lat']
long = attachment['payload']['coordinates']['long'] long = attachment['payload']['coordinates']['long']
@@ -167,7 +128,7 @@ class Messages::Facebook::MessageBuilder
result = {} result = {}
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user # OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
# We don't need to capture this error as we don't care about contact params in case of echo messages # We don't need to capture this error as we don't care about contact params in case of echo messages
Sentry.capture_exception(e) unless outgoing_echo? Sentry.capture_exception(e) unless @outgoing_echo
rescue StandardError => e rescue StandardError => e
result = {} result = {}
Sentry.capture_exception(e) Sentry.capture_exception(e)

View File

@@ -0,0 +1,150 @@
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
# based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages.
class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
attr_reader :messaging
def initialize(messaging, inbox, outgoing_echo: false)
super()
@messaging = messaging
@inbox = inbox
@outgoing_echo = outgoing_echo
end
def perform
return if @inbox.channel.reauthorization_required?
ActiveRecord::Base.transaction do
build_message
end
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
Sentry.capture_exception(e)
true
end
private
def attachments
@messaging[:message][:attachments] || {}
end
def message_type
@outgoing_echo ? :outgoing : :incoming
end
def message_source_id
@outgoing_echo ? recipient_id : sender_id
end
def sender_id
@messaging[:sender][:id]
end
def recipient_id
@messaging[:recipient][:id]
end
def message
@messaging[:message]
end
def contact
@contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def message_content
@messaging[:message][:text]
end
def content_attributes
{ message_id: @messaging[:message][:mid] }
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
@message = conversation.messages.create!(message_params)
attachments.each do |attachment|
process_attachment(attachment)
end
end
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
))
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id,
additional_attributes: {
type: 'instagram_direct_message'
}
}
end
def message_params
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: message_type,
source_id: message_source_id,
content: message_content,
content_attributes: content_attributes,
sender: @outgoing_echo ? nil : contact
}
end
def already_sent_from_chatwoot?
cw_message = conversation.messages.where(
source_id: nil,
message_type: 'outgoing',
content: message_content,
private: false,
status: :sent
).first
cw_message.update(content_attributes: content_attributes) if cw_message.present?
cw_message.present?
end
### Sample response
# {
# "object": "instagram",
# "entry": [
# {
# "id": "<IGID>",// ig id of the business
# "time": 1569262486134,
# "messaging": [
# {
# "sender": {
# "id": "<IGSID>"
# },
# "recipient": {
# "id": "<IGID>"
# },
# "timestamp": 1569262485349,
# "message": {
# "mid": "<MESSAGE_ID>",
# "text": "<MESSAGE_CONTENT>"
# }
# }
# ]
# }
# ],
# }
end

View File

@@ -16,6 +16,7 @@ class Messages::MessageBuilder
def perform def perform
@message = @conversation.messages.build(message_params) @message = @conversation.messages.build(message_params)
process_attachments process_attachments
process_emails
@message.save! @message.save!
@message @message
end end
@@ -34,6 +35,16 @@ class Messages::MessageBuilder
end end
end end
def process_emails
return unless @conversation.inbox&.inbox_type == 'Email'
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
@message.content_attributes[:cc_emails] = cc_emails
@message.content_attributes[:bcc_emails] = bcc_emails
end
def message_type def message_type
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming' if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
raise StandardError, 'Incoming messages are only allowed in Api inboxes' raise StandardError, 'Incoming messages are only allowed in Api inboxes'

View File

@@ -0,0 +1,42 @@
class Messages::Messenger::MessageBuilder
def process_attachment(attachment)
return if attachment['type'].to_sym == :template
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
def attach_file(attachment, file_url)
attachment_file = Down.download(
file_url
)
attachment.file.attach(
io: attachment_file,
filename: attachment_file.original_filename,
content_type: attachment_file.content_type
)
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
end
params
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
end

View File

@@ -41,19 +41,25 @@ class V2::ReportBuilder
user user
when :label when :label
label label
when :team
team
end end
end end
def inbox def inbox
@inbox ||= account.inboxes.where(id: params[:id]).first @inbox ||= account.inboxes.find(params[:id])
end end
def user def user
@user ||= account.users.where(id: params[:id]).first @user ||= account.users.find(params[:id])
end end
def label def label
@label ||= account.labels.where(id: params[:id]).first @label ||= account.labels.find(params[:id])
end
def team
@team ||= account.teams.find(params[:id])
end end
def conversations_count def conversations_count
@@ -62,15 +68,14 @@ class V2::ReportBuilder
.count .count
end end
# unscoped removes all scopes added to a model previously
def incoming_messages_count def incoming_messages_count
scope.messages.unscoped.where(account_id: account.id).incoming scope.messages.incoming.unscope(:order)
.group_by_day(:created_at, range: range, default_value: 0) .group_by_day(:created_at, range: range, default_value: 0)
.count .count
end end
def outgoing_messages_count def outgoing_messages_count
scope.messages.unscoped.where(account_id: account.id).outgoing scope.messages.outgoing.unscope(:order)
.group_by_day(:created_at, range: range, default_value: 0) .group_by_day(:created_at, range: range, default_value: 0)
.count .count
end end

View File

@@ -9,21 +9,18 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
@agents = agents @agents = agents
end end
def create; end
def update
@agent.update!(agent_params.slice(:name).compact)
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
end
def destroy def destroy
@agent.current_account_user.destroy @agent.current_account_user.destroy
head :ok head :ok
end end
def update
@agent.update!(agent_params.except(:role))
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
end
def create
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
end
private private
def check_authorization def check_authorization
@@ -47,22 +44,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end end
def save_account_user def save_account_user
AccountUser.create!( AccountUser.create!({
account_id: Current.account.id, account_id: Current.account.id,
user_id: @user.id, user_id: @user.id,
role: new_agent_params[:role],
inviter_id: current_user.id inviter_id: current_user.id
) }.merge({
role: new_agent_params[:role],
availability: new_agent_params[:availability],
auto_offline: new_agent_params[:auto_offline]
}.compact))
end end
def agent_params def agent_params
params.require(:agent).permit(:email, :name, :role) params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
end end
def new_agent_params def new_agent_params
# intial string ensures the password requirements are met # intial string ensures the password requirements are met
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
params.require(:agent).permit(:email, :name, :role) params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user) .merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
end end

View File

@@ -12,9 +12,10 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
page_access_token: page_access_token page_access_token: page_access_token
) )
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel) @facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_instagram_id(page_access_token, facebook_channel)
set_avatar(@facebook_inbox, page_id) set_avatar(@facebook_inbox, page_id)
rescue StandardError => e rescue StandardError => e
Rails.logger.info e Sentry.capture_exception(e)
end end
end end
@@ -22,6 +23,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts')) @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
end end
def set_instagram_id(page_access_token, facebook_channel)
fb_object = Koala::Facebook::API.new(page_access_token)
response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' })
return if response['instagram_business_account'].blank?
instagram_id = response['instagram_business_account']['id']
facebook_channel.update(instagram_id: instagram_id)
end
# get params[:inbox_id], current_account. params[:omniauth_token] # get params[:inbox_id], current_account. params[:omniauth_token]
def reauthorize_page def reauthorize_page
if @inbox&.facebook? if @inbox&.facebook?
@@ -45,8 +55,13 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
def update_fb_page(fb_page_id, access_token) def update_fb_page(fb_page_id, access_token)
fb_page = get_fb_page(fb_page_id) fb_page = get_fb_page(fb_page_id)
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token) ActiveRecord::Base.transaction do
fb_page&.reauthorized! fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
set_instagram_id(access_token, fb_page)
fb_page&.reauthorized!
rescue StandardError => e
Sentry.capture_exception(e)
end
end end
def get_fb_page(fb_page_id) def get_fb_page(fb_page_id)

View File

@@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
end end
def campaign_params def campaign_params
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id, params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
:scheduled_at, audience: [:type, :id], trigger_rules: {}) :scheduled_at, audience: [:type, :id], trigger_rules: {})
end end
end end

View File

@@ -10,7 +10,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search] before_action :set_current_page, only: [:index, :active, :search]
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes] before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
before_action :set_include_contact_inboxes, only: [:index, :search] before_action :set_include_contact_inboxes, only: [:index, :search]
def index def index
@@ -30,10 +30,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end end
def import def import
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
import = Current.account.data_imports.create!(data_type: 'contacts') import = Current.account.data_imports.create!(data_type: 'contacts')
import.import_file.attach(params[:import_file]) import.import_file.attach(params[:import_file])
end end
head :ok head :ok
end end
@@ -70,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
}, status: :unprocessable_entity }, status: :unprocessable_entity
end end
def destroy
if ::OnlineStatusTracker.get_presence(
@contact.account.id, 'Contact', @contact.id
)
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
:unprocessable_entity)
end
@contact.destroy!
head :ok
end
private private
# TODO: Move this to a finder class # TODO: Move this to a finder class
@@ -134,4 +149,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def fetch_contact def fetch_contact
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def render_error(error, error_status)
render json: error, status: error_status
end
end end

View File

@@ -69,6 +69,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def update_last_seen def update_last_seen
@conversation.agent_last_seen_at = DateTime.now.utc @conversation.agent_last_seen_at = DateTime.now.utc
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
@conversation.save!
end
def custom_attributes
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
@conversation.save! @conversation.save!
end end
@@ -112,6 +118,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def conversation_params def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {} additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {} status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases # TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
@@ -122,11 +129,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
contact_id: @contact_inbox.contact_id, contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id, contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes, additional_attributes: additional_attributes,
snoozed_until: params[:snoozed_until] custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status) }.merge(status)
end end
def conversation_finder def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params) @conversation_finder ||= ConversationFinder.new(current_user, params)
end end
def assignee?
@conversation.assignee_id? && current_user == @conversation.assignee
end
end end

View File

@@ -96,6 +96,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type)) Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
when 'telegram' when 'telegram'
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type)) Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
when 'whatsapp'
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
end end
end end

View File

@@ -1,9 +1,7 @@
class Api::V1::ProfilesController < Api::BaseController class Api::V1::ProfilesController < Api::BaseController
before_action :set_user before_action :set_user
def show def show; end
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
end
def update def update
if password_params[:password].present? if password_params[:password].present?
@@ -15,19 +13,26 @@ class Api::V1::ProfilesController < Api::BaseController
@user.update!(profile_params) @user.update!(profile_params)
end end
def availability
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end
private private
def set_user def set_user
@user = current_user @user = current_user
end end
def availability_params
params.require(:profile).permit(:account_id, :availability)
end
def profile_params def profile_params
params.require(:profile).permit( params.require(:profile).permit(
:email, :email,
:name, :name,
:display_name, :display_name,
:avatar, :avatar,
:availability,
ui_settings: {} ui_settings: {}
) )
end end

View File

@@ -29,6 +29,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv' render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
end end
def teams
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
end
private private
def check_authorization def check_authorization

View File

@@ -0,0 +1,30 @@
class Webhooks::InstagramController < ApplicationController
skip_before_action :authenticate_user!, raise: false
skip_before_action :set_current_user
def verify
if valid_instagram_token?(params['hub.verify_token'])
Rails.logger.info('Instagram webhook verified')
render json: params['hub.challenge']
else
render json: { error: 'Error; wrong verify token', status: 403 }
end
end
def events
Rails.logger.info('Instagram webhook received events')
if params['object'].casecmp('instagram').zero?
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
render json: :ok
else
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
head :unprocessable_entity
end
end
private
def valid_instagram_token?(token)
token == ENV['IG_VERIFY_TOKEN']
end
end

View File

@@ -0,0 +1,6 @@
class Webhooks::WhatsappController < ActionController::API
def process_payload
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
head :ok
end
end

View File

@@ -94,6 +94,8 @@ class ConversationFinder
end end
def filter_by_status def filter_by_status
return if params[:status] == 'all'
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS) @conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
end end

View File

@@ -8,6 +8,7 @@
:has-accounts="hasAccounts" :has-accounts="hasAccounts"
/> />
<woot-snackbar-box /> <woot-snackbar-box />
<network-notification />
</div> </div>
</template> </template>
@@ -15,6 +16,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal'; import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import WootSnackbarBox from './components/SnackbarContainer'; import WootSnackbarBox from './components/SnackbarContainer';
import NetworkNotification from './components/NetworkNotification';
import { accountIdFromPathname } from './helper/URLHelper'; import { accountIdFromPathname } from './helper/URLHelper';
export default { export default {
@@ -23,6 +25,7 @@ export default {
components: { components: {
WootSnackbarBox, WootSnackbarBox,
AddAccountModal, AddAccountModal,
NetworkNotification,
}, },
data() { data() {

View File

@@ -0,0 +1,18 @@
/* global axios */
import ApiClient from './ApiClient';
class AccountActions extends ApiClient {
constructor() {
super('actions', { accountScoped: true });
}
merge(parentId, childId) {
return axios.post(`${this.url}/contact_merge`, {
base_contact_id: parentId,
mergee_contact_id: childId,
});
}
}
export default new AccountActions();

View File

@@ -161,9 +161,9 @@ export default {
}); });
}, },
updateAvailability({ availability }) { updateAvailability(availabilityData) {
return axios.put(endPoints('profileUpdate').url, { return axios.post(endPoints('availabilityUpdate').url, {
profile: { availability }, profile: { ...availabilityData },
}); });
}, },
}; };

View File

@@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
)}`; )}`;
return axios.get(requestURL); return axios.get(requestURL);
} }
importContacts(file) {
const formData = new FormData();
formData.append('import_file', file);
return axios.post(`${this.url}/import`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
} }
export default new ContactAPI(); export default new ContactAPI();

View File

@@ -13,6 +13,9 @@ const endPoints = {
profileUpdate: { profileUpdate: {
url: '/api/v1/profile', url: '/api/v1/profile',
}, },
availabilityUpdate: {
url: '/api/v1/profile/availability',
},
logout: { logout: {
url: 'auth/sign_out', url: 'auth/sign_out',
}, },

View File

@@ -8,6 +8,8 @@ export const buildCreatePayload = ({
contentAttributes, contentAttributes,
echoId, echoId,
file, file,
ccEmails,
bccEmails,
}) => { }) => {
let payload; let payload;
if (file) { if (file) {
@@ -18,12 +20,16 @@ export const buildCreatePayload = ({
} }
payload.append('private', isPrivate); payload.append('private', isPrivate);
payload.append('echo_id', echoId); payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails);
} else { } else {
payload = { payload = {
content: message, content: message,
private: isPrivate, private: isPrivate,
echo_id: echoId, echo_id: echoId,
content_attributes: contentAttributes, content_attributes: contentAttributes,
cc_emails: ccEmails,
bcc_emails: bccEmails,
}; };
} }
return payload; return payload;
@@ -41,6 +47,8 @@ class MessageApi extends ApiClient {
contentAttributes, contentAttributes,
echo_id: echoId, echo_id: echoId,
file, file,
ccEmails,
bccEmails,
}) { }) {
return axios({ return axios({
method: 'post', method: 'post',
@@ -51,6 +59,8 @@ class MessageApi extends ApiClient {
contentAttributes, contentAttributes,
echoId, echoId,
file, file,
ccEmails,
bccEmails,
}), }),
}); });
} }

View File

@@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
super('reports', { accountScoped: true, apiVersion: 'v2' }); super('reports', { accountScoped: true, apiVersion: 'v2' });
} }
getAccountReports(metric, since, until) { getReports(metric, since, until, type = 'account', id) {
return axios.get(`${this.url}`, { return axios.get(`${this.url}`, {
params: { metric, since, until, type: 'account' }, params: { metric, since, until, type, id },
}); });
} }
getAccountSummary(since, until) { getSummary(since, until, type = 'account', id) {
return axios.get(`${this.url}/summary`, { return axios.get(`${this.url}/summary`, {
params: { since, until, type: 'account' }, params: { since, until, type, id },
}); });
} }
@@ -23,6 +23,24 @@ class ReportsAPI extends ApiClient {
params: { since, until }, params: { since, until },
}); });
} }
getLabelReports(since, until) {
return axios.get(`${this.url}/labels`, {
params: { since, until },
});
}
getInboxReports(since, until) {
return axios.get(`${this.url}/inboxes`, {
params: { since, until },
});
}
getTeamReports(since, until) {
return axios.get(`${this.url}/teams`, {
params: { since, until },
});
}
} }
export default new ReportsAPI(); export default new ReportsAPI();

View File

@@ -0,0 +1,23 @@
import accountActionsAPI from '../accountActions';
import ApiClient from '../ApiClient';
import describeWithAPIMock from './apiSpecHelper';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
expect(accountActionsAPI).toBeInstanceOf(ApiClient);
expect(accountActionsAPI).toHaveProperty('merge');
});
describeWithAPIMock('API calls', context => {
it('#merge', () => {
accountActionsAPI.merge(1, 2);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/actions/contact_merge',
{
base_contact_id: 1,
mergee_contact_id: 2,
}
);
});
});
});

View File

@@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support' '/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
); );
}); });
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import',
expect.any(FormData),
{
headers: { 'Content-Type': 'multipart/form-data' },
}
);
});
}); });
}); });

View File

@@ -11,39 +11,35 @@ describe('#Reports API', () => {
expect(reportsAPI).toHaveProperty('create'); expect(reportsAPI).toHaveProperty('create');
expect(reportsAPI).toHaveProperty('update'); expect(reportsAPI).toHaveProperty('update');
expect(reportsAPI).toHaveProperty('delete'); expect(reportsAPI).toHaveProperty('delete');
expect(reportsAPI).toHaveProperty('getAccountReports'); expect(reportsAPI).toHaveProperty('getReports');
expect(reportsAPI).toHaveProperty('getAccountSummary'); expect(reportsAPI).toHaveProperty('getSummary');
expect(reportsAPI).toHaveProperty('getAgentReports'); expect(reportsAPI).toHaveProperty('getAgentReports');
expect(reportsAPI).toHaveProperty('getLabelReports');
expect(reportsAPI).toHaveProperty('getInboxReports');
expect(reportsAPI).toHaveProperty('getTeamReports');
}); });
describeWithAPIMock('API calls', context => { describeWithAPIMock('API calls', context => {
it('#getAccountReports', () => { it('#getAccountReports', () => {
reportsAPI.getAccountReports( reportsAPI.getReports('conversations_count', 1621103400, 1621621800);
'conversations_count', expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
1621103400, params: {
1621621800 metric: 'conversations_count',
); since: 1621103400,
expect(context.axiosMock.get).toHaveBeenCalledWith( until: 1621621800,
'/api/v2/reports', type: 'account',
{ },
params: { });
metric: 'conversations_count',
since: 1621103400,
until: 1621621800,
type: 'account'
},
}
);
}); });
it('#getAccountSummary', () => { it('#getAccountSummary', () => {
reportsAPI.getAccountSummary(1621103400, 1621621800); reportsAPI.getSummary(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith( expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/summary', '/api/v2/reports/summary',
{ {
params: { params: {
since: 1621103400, since: 1621103400,
until: 1621621800, until: 1621621800,
type: 'account' type: 'account',
}, },
} }
); );
@@ -61,5 +57,44 @@ describe('#Reports API', () => {
} }
); );
}); });
it('#getLabelReports', () => {
reportsAPI.getLabelReports(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/labels',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getInboxReports', () => {
reportsAPI.getInboxReports(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/inboxes',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
it('#getTeamReports', () => {
reportsAPI.getTeamReports(1621103400, 1621621800);
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/teams',
{
params: {
since: 1621103400,
until: 1621621800,
},
}
);
});
}); });
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -93,3 +93,17 @@
/* .slide-fade-leave-active for <2.1.8 */ { /* .slide-fade-leave-active for <2.1.8 */ {
opacity: 0; opacity: 0;
} }
.network-notification-fade-enter-active {
transition: all .1s $ease-in-sine;
}
.network-notification-fade-leave-active {
transition: all .1s $ease-out-sine;
}
.network-notification-fade-enter,
.network-notification-fade-leave-to {
transform: translateY(-$space-small);
opacity: 0;
}

View File

@@ -0,0 +1,3 @@
.margin-right-small {
margin-right: var(--space-small);
}

View File

@@ -14,6 +14,7 @@
@import 'helper-classes'; @import 'helper-classes';
@import 'formulate'; @import 'formulate';
@import 'date-picker'; @import 'date-picker';
@import 'utility-helpers';
@import 'foundation-sites/scss/foundation'; @import 'foundation-sites/scss/foundation';
@import '~bourbon/core/bourbon'; @import '~bourbon/core/bourbon';

View File

@@ -15,6 +15,10 @@
.multiselect { .multiselect {
margin-bottom: var(--space-normal); margin-bottom: var(--space-normal);
&.multiselect--disabled {
opacity: .8;
}
.multiselect--active { .multiselect--active {
>.multiselect__tags { >.multiselect__tags {
border-color: $color-woot; border-color: $color-woot;
@@ -209,3 +213,53 @@
flex-shrink: 0; flex-shrink: 0;
} }
} }
.multiselect-wrap--medium {
$multiselect-height: 4.8rem;
.multiselect__tags,
.multiselect__input {
align-items: center;
display: flex;
}
.multiselect__tags,
.multiselect__input,
.multiselect {
background: var(--white);
font-size: var(--font-size-small);
height: $multiselect-height;
min-height: $multiselect-height;
}
.multiselect__input {
height: $multiselect-height - $space-micro;
min-height: $multiselect-height - $space-micro;
}
.multiselect__single {
align-items: center;
display: flex;
font-size: var(--font-size-small);
margin: 0;
padding: var(--space-smaller) var(--space-micro);
}
.multiselect__placeholder {
margin: 0;
padding: var(--space-smaller) var(--space-micro);
}
.multiselect__select {
min-height: $multiselect-height;
}
.multiselect--disabled .multiselect__current,
.multiselect--disabled .multiselect__select {
background: transparent;
}
.multiselect__tags-wrap {
flex-shrink: 0;
}
}

View File

@@ -42,14 +42,6 @@ $resolve-button-width: 13.2rem;
margin-right: var(--space-normal); margin-right: var(--space-normal);
min-width: 0; min-width: 0;
.user--name {
@include margin(0);
display: inline-block;
font-size: $font-size-medium;
line-height: 1.3;
text-transform: capitalize;
width: 100%;
}
.user--profile__meta { .user--profile__meta {
align-items: flex-start; align-items: flex-start;
@@ -59,12 +51,6 @@ $resolve-button-width: 13.2rem;
margin-left: $space-slab; margin-left: $space-slab;
min-width: 0; min-width: 0;
} }
.user--profile__button {
font-size: $font-size-mini;
margin-top: $space-micro;
padding: 0;
}
} }
} }

View File

@@ -93,7 +93,7 @@
.conversation-panel { .conversation-panel {
@include flex; @include flex;
@include flex-weight(1); @include flex-weight(1 1 1px);
@include margin($zero); @include margin($zero);
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;

View File

@@ -71,7 +71,8 @@
@include padding($space-large); @include padding($space-large);
} }
form { form,
.modal-content {
@include padding($space-large); @include padding($space-large);
align-self: center; align-self: center;

View File

@@ -32,7 +32,6 @@
} }
} }
.report-bar { .report-bar {
@include margin(-1px $zero); @include margin(-1px $zero);
@include background-white; @include background-white;

View File

@@ -0,0 +1,30 @@
.date-picker {
margin-left: var(--space-smaller);
}
.margin-left-small {
margin-left: var(--space-smaller);
}
.reports-option__rounded--item {
border-radius: 100%;
height: var(--space-two);
width: var(--space-two);
}
.reports-option__item {
flex-shrink: 0;
margin-right: var(--space-small);
}
.reports-option__label--swatch {
border: 1px solid var(--color-border);
}
.margin-right-small {
margin-right: var(--space-small);
}
.display-flex {
display: flex;
}

View File

@@ -26,6 +26,7 @@
:active-label="label" :active-label="label"
:team-id="teamId" :team-id="teamId"
:chat="chat" :chat="chat"
:show-assignee="showAssigneeInConversationCard"
/> />
<div v-if="chatListLoading" class="text-center"> <div v-if="chatListLoading" class="text-center">
@@ -119,6 +120,9 @@ export default {
}; };
}); });
}, },
showAssigneeInConversationCard() {
return this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL;
},
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox); return this.$store.getters['inboxes/getInbox'](this.activeInbox);
}, },

View File

@@ -0,0 +1,123 @@
<template>
<transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="ui-notification-container">
<div class="ui-notification">
<svg
class="ui-notification-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
/>
</svg>
<p class="ui-notification-text">
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
</p>
<button class="ui-refresh-button" @click="refreshPage">
{{ $t('NETWORK.BUTTON.REFRESH') }}
</button>
<button class="ui-close-button" @click="closeNotification">
<i class="ui-close-icon icon ion-ios-close-outline" />
</button>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
showNotification: !navigator.onLine,
};
},
mounted() {
window.addEventListener('offline', this.updateOnlineStatus);
},
beforeDestroy() {
window.removeEventListener('offline', this.updateOnlineStatus);
},
methods: {
refreshPage() {
window.location.reload();
},
closeNotification() {
this.showNotification = false;
},
updateOnlineStatus(event) {
if (event.type === 'offline') {
this.showNotification = true;
}
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.ui-notification-container {
max-width: 40rem;
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
width: 100%;
z-index: 9999;
}
.ui-notification {
@include shadow;
align-items: center;
background-color: var(--white);
border: 1px solid var(--color-border);
border-radius: var(--space-one);
display: flex;
justify-content: space-between;
max-width: 40rem;
min-height: 3rem;
min-width: 24rem;
padding: var(--space-normal) var(--space-two);
text-align: left;
}
.ui-notification-text {
margin: 0;
}
.ui-refresh-button {
color: var(--color-woot);
font-size: var(--font-size-small);
font-weight: bold;
&:hover {
cursor: pointer;
}
}
.ui-notification-icon {
color: var(--b-600);
width: var(--font-size-mega);
}
.ui-close-icon {
color: var(--b-600);
font-size: var(--font-size-large);
}
.ui-close-button {
&:hover {
cursor: pointer;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="status"> <div class="status">
<div class="status-view"> <div class="status-view">
<availability-status-badge :status="currentUserAvailabilityStatus" /> <availability-status-badge :status="currentUserAvailability" />
<div class="status-view--title"> <div class="status-view--title">
{{ availabilityDisplayLabel }} {{ availabilityDisplayLabel }}
</div> </div>
@@ -26,7 +26,9 @@
color-scheme="secondary" color-scheme="secondary"
class-names="status-change--dropdown-button" class-names="status-change--dropdown-button"
:is-disabled="status.disabled" :is-disabled="status.disabled"
@click="changeAvailabilityStatus(status.value)" @click="
changeAvailabilityStatus(status.value, currentAccountId)
"
> >
<availability-status-badge :status="status.value" /> <availability-status-badge :status="status.value" />
{{ status.label }} {{ status.label }}
@@ -75,18 +77,22 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', getCurrentUserAvailability: 'getCurrentUserAvailability',
getCurrentAccountId: 'getCurrentAccountId',
}), }),
availabilityDisplayLabel() { availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex( const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
key => key === this.currentUserAvailabilityStatus key => key === this.currentUserAvailability
); );
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[ return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[
availabilityIndex availabilityIndex
]; ];
}, },
currentUserAvailabilityStatus() { currentAccountId() {
return this.currentUser.availability_status; return this.getCurrentAccountId;
},
currentUserAvailability() {
return this.getCurrentUserAvailability;
}, },
availabilityStatuses() { availabilityStatuses() {
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map( return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
@@ -94,7 +100,7 @@ export default {
label: statusLabel, label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index], value: AVAILABILITY_STATUS_KEYS[index],
disabled: disabled:
this.currentUserAvailabilityStatus === this.currentUserAvailability ===
AVAILABILITY_STATUS_KEYS[index], AVAILABILITY_STATUS_KEYS[index],
}) })
); );
@@ -108,16 +114,16 @@ export default {
closeStatusMenu() { closeStatusMenu() {
this.isStatusMenuOpened = false; this.isStatusMenuOpened = false;
}, },
changeAvailabilityStatus(availability) { changeAvailabilityStatus(availability, accountId) {
if (this.isUpdating) { if (this.isUpdating) {
return; return;
} }
this.isUpdating = true; this.isUpdating = true;
this.$store this.$store
.dispatch('updateAvailability', { .dispatch('updateAvailability', {
availability, availability: availability,
account_id: accountId,
}) })
.finally(() => { .finally(() => {
this.isUpdating = false; this.isUpdating = false;

View File

@@ -17,7 +17,8 @@ const i18nConfig = new VueI18n({
}); });
describe('AvailabilityStatus', () => { describe('AvailabilityStatus', () => {
const currentUser = { availability_status: 'online' }; const currentAvailability = 'online';
const currentAccountId = '1';
let store = null; let store = null;
let actions = null; let actions = null;
let modules = null; let modules = null;
@@ -33,7 +34,8 @@ describe('AvailabilityStatus', () => {
modules = { modules = {
auth: { auth: {
getters: { getters: {
getCurrentUser: () => currentUser, getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
}, },
}, },
}; };
@@ -77,7 +79,7 @@ describe('AvailabilityStatus', () => {
expect(actions.updateAvailability).toBeCalledWith( expect(actions.updateAvailability).toBeCalledWith(
expect.any(Object), expect.any(Object),
{ availability: 'offline' }, { availability: 'offline', account_id: currentAccountId },
undefined undefined
); );
}); });

View File

@@ -6,7 +6,7 @@
> >
<img <img
v-if="channel.key === 'facebook'" v-if="channel.key === 'facebook'"
src="~dashboard/assets/images/channels/facebook.png" src="~dashboard/assets/images/channels/messenger.png"
/> />
<img <img
v-if="channel.key === 'twitter'" v-if="channel.key === 'twitter'"

View File

@@ -0,0 +1,35 @@
<template>
<span class="inbox--name">
<i :class="computedInboxClass" />
{{ inbox.name }}
</span>
</template>
<script>
import { getInboxClassByType } from 'dashboard/helper/inbox';
export default {
props: {
inbox: {
type: Object,
default: () => {},
},
},
computed: {
computedInboxClass() {
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
const classByType = getInboxClassByType(type, phoneNumber);
return classByType;
},
},
};
</script>
<style scoped>
.inbox--name {
padding: var(--space-micro) 0;
line-height: var(--space-slab);
font-weight: var(--font-weight-medium);
background: none;
color: var(--s-500);
font-size: var(--font-size-mini);
}
</style>

View File

@@ -15,39 +15,60 @@
:size="avatarSize" :size="avatarSize"
/> />
<img <img
v-if="badge === 'Channel::FacebookPage'" v-if="badge === 'instagram_direct_message'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/fb-badge.png" src="/integrations/channels/badges/instagram-dm.png"
/> />
<img <img
v-if="badge === 'Channel::TwitterProfile'" v-else-if="badge === 'facebook'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png" src="/integrations/channels/badges/messenger.png"
/> />
<img <img
v-if="badge === 'Channel::TwilioSms'" v-else-if="badge === 'twitter-tweet'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/channels/whatsapp.png" src="/integrations/channels/badges/twitter-tweet.png"
/> />
<img <img
v-if="badge === 'Channel::Line'" v-else-if="badge === 'twitter-dm'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/channels/line.png" src="/integrations/channels/badges/twitter-dm.png"
/> />
<img <img
v-if="badge === 'Channel::Telegram'" v-else-if="badge === 'whatsapp'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/channels/telegram.png" src="/integrations/channels/badges/whatsapp.png"
/>
<img
v-else-if="badge === 'sms'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/sms.png"
/>
<img
v-else-if="badge === 'Channel::Line'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/line.png"
/>
<img
v-else-if="badge === 'Channel::Telegram'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="/integrations/channels/badges/telegram.png"
/> />
<div <div
v-if="showStatusIndicator" v-if="showStatusIndicator"
@@ -109,8 +130,10 @@ export default {
return Number(this.size.replace(/\D+/g, '')); return Number(this.size.replace(/\D+/g, ''));
}, },
badgeStyle() { badgeStyle() {
const badgeSize = `${this.avatarSize / 3}px`; const size = Math.floor(this.avatarSize / 3);
return { width: badgeSize, height: badgeSize }; const badgeSize = `${size + 2}px`;
const borderRadius = `${size / 2}px`;
return { width: badgeSize, height: badgeSize, borderRadius };
}, },
statusStyle() { statusStyle() {
const statusSize = `${this.avatarSize / 4}px`; const statusSize = `${this.avatarSize / 4}px`;
@@ -152,6 +175,7 @@ export default {
height: 100%; height: 100%;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
object-fit: cover;
&.border { &.border {
border: 1px solid white; border: 1px solid white;
@@ -159,8 +183,12 @@ export default {
} }
.source-badge { .source-badge {
background: white;
border-radius: var(--border-radius-small);
bottom: -$space-micro; bottom: -$space-micro;
box-shadow: var(--shadow-small);
height: $space-slab; height: $space-slab;
padding: var(--space-micro);
position: absolute; position: absolute;
right: $zero; right: $zero;
width: $space-slab; width: $space-slab;

View File

@@ -7,6 +7,9 @@
> >
{{ item['TEXT'] }} {{ item['TEXT'] }}
</option> </option>
<option value="all">
{{ $t('CHAT_LIST.FILTER_ALL') }}
</option>
</select> </select>
</template> </template>
@@ -30,6 +33,8 @@ export default {
} else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) { } else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) {
this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED; this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED;
} else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) { } else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) {
this.activeStatus = wootConstants.STATUS_TYPE.ALL;
} else if (this.activeStatus === wootConstants.STATUS_TYPE.ALL) {
this.activeStatus = wootConstants.STATUS_TYPE.OPEN; this.activeStatus = wootConstants.STATUS_TYPE.OPEN;
} }
} }

View File

@@ -8,20 +8,26 @@
}" }"
@click="cardClick(chat)" @click="cardClick(chat)"
> >
<Thumbnail <thumbnail
v-if="!hideThumbnail" v-if="!hideThumbnail"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:badge="chatMetadata.channel" :badge="inboxBadge"
class="columns" class="columns"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status" :status="currentContact.availability_status"
size="40px" size="40px"
/> />
<div class="conversation--details columns"> <div class="conversation--details columns">
<span v-if="showInboxName" class="label"> <div class="conversation--metadata">
<i :class="computedInboxClass" /> <inbox-name v-if="showInboxName" :inbox="inbox" />
{{ inboxName }} <span
</span> v-if="showAssignee && assignee.name"
class="label assignee-label text-truncate"
>
<i class="ion-person" />
{{ assignee.name }}
</span>
</div>
<h4 class="conversation--user"> <h4 class="conversation--user">
{{ currentContact.name }} {{ currentContact.name }}
</h4> </h4>
@@ -62,19 +68,21 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants'; import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { getInboxClassByType } from 'dashboard/helper/inbox';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time'; import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper'; import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName';
import inboxMixin from 'shared/mixins/inboxMixin';
export default { export default {
components: { components: {
InboxName,
Thumbnail, Thumbnail,
}, },
mixins: [timeMixin, conversationMixin, messageFormatterMixin], mixins: [inboxMixin, timeMixin, conversationMixin, messageFormatterMixin],
props: { props: {
activeLabel: { activeLabel: {
type: String, type: String,
@@ -96,6 +104,10 @@ export default {
type: [String, Number], type: [String, Number],
default: 0, default: 0,
}, },
showAssignee: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
@@ -108,7 +120,11 @@ export default {
}), }),
chatMetadata() { chatMetadata() {
return this.chat.meta; return this.chat.meta || {};
},
assignee() {
return this.chatMetadata.assignee || {};
}, },
currentContact() { currentContact() {
@@ -167,18 +183,12 @@ export default {
return this.getPlainText(subject || this.lastMessageInChat.content); return this.getPlainText(subject || this.lastMessageInChat.content);
}, },
chatInbox() { inbox() {
const { inbox_id: inboxId } = this.chat; const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId); const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox; return stateInbox;
}, },
computedInboxClass() {
const { phone_number: phoneNumber, channel_type: type } = this.chatInbox;
const classByType = getInboxClassByType(type, phoneNumber);
return classByType;
},
showInboxName() { showInboxName() {
return ( return (
!this.hideInboxName && !this.hideInboxName &&
@@ -187,11 +197,10 @@ export default {
); );
}, },
inboxName() { inboxName() {
const stateInbox = this.chatInbox; const stateInbox = this.inbox;
return stateInbox.name || ''; return stateInbox.name || '';
}, },
}, },
methods: { methods: {
cardClick(chat) { cardClick(chat) {
const { activeInbox } = this; const { activeInbox } = this;
@@ -226,15 +235,6 @@ export default {
} }
} }
.conversation--details .label {
padding: var(--space-micro) 0 var(--space-micro) 0;
line-height: var(--space-slab);
font-weight: var(--font-weight-medium);
background: none;
color: var(--s-500);
font-size: var(--font-size-mini);
}
.conversation--details { .conversation--details {
.conversation--user { .conversation--user {
padding-top: var(--space-micro); padding-top: var(--space-micro);
@@ -252,4 +252,23 @@ export default {
color: var(--s-600); color: var(--s-600);
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
} }
.conversation--metadata {
display: flex;
justify-content: space-between;
padding-right: var(--space-normal);
.label {
padding: var(--space-micro) 0 var(--space-micro) 0;
line-height: var(--space-slab);
font-weight: var(--font-weight-medium);
background: none;
color: var(--s-500);
font-size: var(--font-size-mini);
}
.assignee-label {
max-width: 50%;
}
}
</style> </style>

View File

@@ -4,7 +4,7 @@
<Thumbnail <Thumbnail
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
size="40px" size="40px"
:badge="chatMetadata.channel" :badge="inboxBadge"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status" :status="currentContact.availability_status"
/> />
@@ -12,20 +12,23 @@
<h3 class="user--name text-truncate"> <h3 class="user--name text-truncate">
{{ currentContact.name }} {{ currentContact.name }}
</h3> </h3>
<woot-button <div class="conversation--header--actions">
class="user--profile__button" <inbox-name :inbox="inbox" class="margin-right-small" />
size="small" <span
variant="link" v-if="isSnoozed"
@click="$emit('contact-panel-toggle')" class="snoozed--display-text margin-right-small"
> >
{{ {{ snoozedDisplayText }}
`${ </span>
isContactPanelOpen <woot-button
? $t('CONVERSATION.HEADER.CLOSE') class="user--profile__button margin-right-small"
: $t('CONVERSATION.HEADER.OPEN') size="small"
} ${$t('CONVERSATION.HEADER.DETAILS')}` variant="link"
}} @click="$emit('contact-panel-toggle')"
</woot-button> >
{{ contactPanelToggleText }}
</woot-button>
</div>
</div> </div>
</div> </div>
<div <div
@@ -42,14 +45,19 @@ import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import agentMixin from '../../../mixins/agentMixin.js'; import agentMixin from '../../../mixins/agentMixin.js';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import inboxMixin from 'shared/mixins/inboxMixin';
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers'; import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import wootConstants from '../../../constants';
import differenceInHours from 'date-fns/differenceInHours';
import InboxName from '../InboxName';
export default { export default {
components: { components: {
InboxName,
MoreActions, MoreActions,
Thumbnail, Thumbnail,
}, },
mixins: [agentMixin, eventListenerMixins], mixins: [inboxMixin, agentMixin, eventListenerMixins],
props: { props: {
chat: { chat: {
type: Object, type: Object,
@@ -60,14 +68,6 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
currentChatAssignee: null,
inboxId: null,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
uiFlags: 'inboxAssignableAgents/getUIFlags', uiFlags: 'inboxAssignableAgents/getUIFlags',
@@ -83,10 +83,37 @@ export default {
this.chat.meta.sender.id this.chat.meta.sender.id
); );
}, },
}, isSnoozed() {
mounted() { return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
const { inbox_id: inboxId } = this.chat; },
this.inboxId = inboxId; snoozedDisplayText() {
const { snoozed_until: snoozedUntil } = this.currentChat;
if (snoozedUntil) {
// When the snooze is applied, it schedules the unsnooze event to next day/week 9AM.
// By that logic if the time difference is less than or equal to 24 + 9 hours we can consider it tomorrow.
const MAX_TIME_DIFFERENCE = 33;
const isSnoozedUntilTomorrow =
differenceInHours(new Date(snoozedUntil), new Date()) <=
MAX_TIME_DIFFERENCE;
return this.$t(
isSnoozedUntilTomorrow
? 'CONVERSATION.HEADER.SNOOZED_UNTIL_TOMORROW'
: 'CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_WEEK'
);
}
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
},
contactPanelToggleText() {
return `${
this.isContactPanelOpen
? this.$t('CONVERSATION.HEADER.CLOSE')
: this.$t('CONVERSATION.HEADER.OPEN')
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
},
inbox() {
const { inbox_id: inboxId } = this.chat;
return this.$store.getters['inboxes/getInbox'](inboxId);
},
}, },
methods: { methods: {
@@ -122,4 +149,28 @@ export default {
flex-shrink: 0; flex-shrink: 0;
} }
} }
.user--name {
display: inline-block;
font-size: var(--font-size-medium);
line-height: 1.3;
margin: 0;
text-transform: capitalize;
width: 100%;
}
.conversation--header--actions {
align-items: center;
display: flex;
font-size: var(--font-size-mini);
.user--profile__button {
padding: 0;
}
.snoozed--display-text {
font-weight: var(--font-weight-medium);
color: var(--y-900);
}
}
</style> </style>

View File

@@ -3,8 +3,9 @@
<div :class="wrapClass"> <div :class="wrapClass">
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass"> <div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
<bubble-mail-head <bubble-mail-head
v-if="isEmailContentType"
:email-attributes="contentAttributes.email" :email-attributes="contentAttributes.email"
:cc="emailHeadAttributes.cc"
:bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming" :is-incoming="isIncoming"
/> />
<bubble-text <bubble-text
@@ -222,6 +223,13 @@ export default {
isIncoming() { isIncoming() {
return this.data.message_type === MESSAGE_TYPE.INCOMING; return this.data.message_type === MESSAGE_TYPE.INCOMING;
}, },
emailHeadAttributes() {
return {
email: this.contentAttributes.email,
cc: this.contentAttributes.cc_emails,
bcc: this.contentAttributes.bcc_emails
}
},
hasAttachments() { hasAttachments() {
return !!(this.data.attachments && this.data.attachments.length > 0); return !!(this.data.attachments && this.data.attachments.length > 0);
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="view-box fill-height"> <div class="view-box fill-height">
<div <div
v-if="!currentChat.can_reply && !isATwilioWhatsappChannel" v-if="!currentChat.can_reply && !isAWhatsappChannel"
class="banner messenger-policy--banner" class="banner messenger-policy--banner"
> >
<span> <span>
@@ -16,7 +16,7 @@
</span> </span>
</div> </div>
<div <div
v-if="!currentChat.can_reply && isATwilioWhatsappChannel" v-if="!currentChat.can_reply && isAWhatsappChannel"
class="banner messenger-policy--banner" class="banner messenger-policy--banner"
> >
<span> <span>

View File

@@ -1,41 +1,29 @@
<template> <template>
<div class="flex-container actions--container"> <div class="flex-container actions--container">
<woot-button
v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
class="hollow secondary actions--button"
icon="ion-volume-mute"
@click="mute"
/>
<woot-button
v-else
v-tooltip.left="$t('CONTACT_PANEL.UNMUTE_CONTACT')"
class="hollow secondary actions--button"
icon="ion-volume-medium"
@click="unmute"
/>
<woot-button
v-tooltip="$t('CONTACT_PANEL.SEND_TRANSCRIPT')"
class="hollow secondary actions--button"
icon="ion-share"
@click="toggleEmailActionsModal"
/>
<resolve-action <resolve-action
:conversation-id="currentChat.id" :conversation-id="currentChat.id"
:status="currentChat.status" :status="currentChat.status"
/> />
<woot-button
class="more--button"
variant="clear"
size="large"
color-scheme="secondary"
icon="ion-android-more-vertical"
@click="toggleConversationActions"
/>
<div
v-if="showConversationActions"
v-on-clickaway="hideConversationActions"
class="dropdown-pane dropdowm--bottom"
:class="{ 'dropdown-pane--open': showConversationActions }"
>
<woot-dropdown-menu>
<woot-dropdown-item v-if="!currentChat.muted">
<button class="button clear alert " @click="mute">
<span>{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}</span>
</button>
</woot-dropdown-item>
<woot-dropdown-item v-else>
<button class="button clear alert" @click="unmute">
<span>{{ $t('CONTACT_PANEL.UNMUTE_CONTACT') }}</span>
</button>
</woot-dropdown-item>
<woot-dropdown-item>
<button class="button clear" @click="toggleEmailActionsModal">
{{ $t('CONTACT_PANEL.SEND_TRANSCRIPT') }}
</button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
<email-transcript-modal <email-transcript-modal
v-if="showEmailActionsModal" v-if="showEmailActionsModal"
:show="showEmailActionsModal" :show="showEmailActionsModal"
@@ -50,13 +38,9 @@ import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import EmailTranscriptModal from './EmailTranscriptModal'; import EmailTranscriptModal from './EmailTranscriptModal';
import ResolveAction from '../../buttons/ResolveAction'; import ResolveAction from '../../buttons/ResolveAction';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
export default { export default {
components: { components: {
WootDropdownMenu,
WootDropdownItem,
EmailTranscriptModal, EmailTranscriptModal,
ResolveAction, ResolveAction,
}, },
@@ -97,7 +81,16 @@ export default {
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~dashboard/assets/scss/mixins'; .actions--container {
align-items: center;
.button {
font-size: var(--font-size-large);
margin-right: var(--space-small);
border-color: var(--color-border);
color: var(--s-400);
}
}
.more--button { .more--button {
align-items: center; align-items: center;

View File

@@ -20,6 +20,11 @@
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> />
<reply-email-head
v-if="showReplyHead"
:clear-mails="clearMails"
@set-emails="setCcEmails"
/>
<resizable-text-area <resizable-text-area
v-if="!showRichContentEditor" v-if="!showRichContentEditor"
ref="messageInput" ref="messageInput"
@@ -82,6 +87,7 @@ import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea'; import ResizableTextArea from 'shared/components/ResizableTextArea';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview'; import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel'; import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
import ReplyEmailHead from './ReplyEmailHead';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel'; import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
@@ -104,6 +110,7 @@ export default {
ResizableTextArea, ResizableTextArea,
AttachmentPreview, AttachmentPreview,
ReplyTopPanel, ReplyTopPanel,
ReplyEmailHead,
ReplyBottomPanel, ReplyBottomPanel,
WootMessageEditor, WootMessageEditor,
}, },
@@ -134,6 +141,7 @@ export default {
mentionSearchKey: '', mentionSearchKey: '',
hasUserMention: false, hasUserMention: false,
hasSlashCommand: false, hasSlashCommand: false,
clearMails: false,
}; };
}, },
computed: { computed: {
@@ -156,7 +164,7 @@ export default {
return !!this.uiSettings.enter_to_send_enabled; return !!this.uiSettings.enter_to_send_enabled;
}, },
isPrivate() { isPrivate() {
if (this.currentChat.can_reply || this.isATwilioWhatsappChannel) { if (this.currentChat.can_reply || this.isAWhatsappChannel) {
return this.isOnPrivateNote; return this.isOnPrivateNote;
} }
return true; return true;
@@ -203,7 +211,7 @@ export default {
if (this.isAFacebookInbox) { if (this.isAFacebookInbox) {
return MESSAGE_MAX_LENGTH.FACEBOOK; return MESSAGE_MAX_LENGTH.FACEBOOK;
} }
if (this.isATwilioWhatsappChannel) { if (this.isAWhatsappChannel) {
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP; return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
} }
if (this.isATwilioSMSChannel) { if (this.isATwilioSMSChannel) {
@@ -223,7 +231,8 @@ export default {
this.isATwilioWhatsappChannel || this.isATwilioWhatsappChannel ||
this.isAPIInbox || this.isAPIInbox ||
this.isAnEmailChannel || this.isAnEmailChannel ||
this.isATwilioSMSChannel this.isATwilioSMSChannel ||
this.isATelegramChannel
); );
}, },
replyButtonLabel() { replyButtonLabel() {
@@ -269,6 +278,9 @@ export default {
} }
return !this.message.trim().replace(/\n/g, '').length; return !this.message.trim().replace(/\n/g, '').length;
}, },
showReplyHead() {
return !this.isOnPrivateNote && this.isAnEmailChannel;
},
}, },
watch: { watch: {
currentChat(conversation) { currentChat(conversation) {
@@ -277,7 +289,7 @@ export default {
return; return;
} }
if (canReply || this.isATwilioWhatsappChannel) { if (canReply || this.isAWhatsappChannel) {
this.replyType = REPLY_EDITOR_MODES.REPLY; this.replyType = REPLY_EDITOR_MODES.REPLY;
} else { } else {
this.replyType = REPLY_EDITOR_MODES.NOTE; this.replyType = REPLY_EDITOR_MODES.NOTE;
@@ -347,9 +359,13 @@ export default {
await this.$store.dispatch('sendMessage', messagePayload); await this.$store.dispatch('sendMessage', messagePayload);
this.$emit('scrollToMessage'); this.$emit('scrollToMessage');
} catch (error) { } catch (error) {
// Error const errorMessage =
error?.response?.data?.error ||
this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage);
} }
this.hideEmojiPicker(); this.hideEmojiPicker();
this.clearMails = false;
} }
}, },
replaceText(message) { replaceText(message) {
@@ -360,7 +376,7 @@ export default {
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) { setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
const { can_reply: canReply } = this.currentChat; const { can_reply: canReply } = this.currentChat;
if (canReply || this.isATwilioWhatsappChannel) this.replyType = mode; if (canReply || this.isAWhatsappChannel) this.replyType = mode;
if (this.showRichContentEditor) { if (this.showRichContentEditor) {
return; return;
} }
@@ -372,6 +388,7 @@ export default {
clearMessage() { clearMessage() {
this.message = ''; this.message = '';
this.attachedFiles = []; this.attachedFiles = [];
this.clearMails = true;
}, },
toggleEmojiPicker() { toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker; this.showEmojiPicker = !this.showEmojiPicker;
@@ -448,11 +465,23 @@ export default {
messagePayload.file = attachment.resource.file; messagePayload.file = attachment.resource.file;
} }
if (this.ccEmails) {
messagePayload.ccEmails = this.ccEmails;
}
if (this.bccEmails) {
messagePayload.bccEmails = this.bccEmails;
}
return messagePayload; return messagePayload;
}, },
setFormatMode(value) { setFormatMode(value) {
this.updateUISettings({ display_rich_content_editor: value }); this.updateUISettings({ display_rich_content_editor: value });
}, },
setCcEmails(value) {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
}, },
}; };
</script> </script>

View File

@@ -1,17 +1,17 @@
<template> <template>
<div> <div>
<div class="input-group-wrap"> <div class="input-group-wrap">
<div class="input-group small" :class="{ error: $v.ccEmails.$error }"> <div class="input-group small" :class="{ error: $v.ccEmailsVal.$error }">
<label class="input-group-label"> <label class="input-group-label">
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }} {{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
</label> </label>
<div class="input-group-field"> <div class="input-group-field">
<woot-input <woot-input
v-model.trim="ccEmails" v-model.trim="$v.ccEmailsVal.$model"
type="email" type="email"
:class="{ error: $v.ccEmails.$error }" :class="{ error: $v.ccEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')" :placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@blur="$v.ccEmails.$touch" @blur="onBlur"
/> />
</div> </div>
<woot-button <woot-button
@@ -23,28 +23,28 @@
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }} {{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
</woot-button> </woot-button>
</div> </div>
<span v-if="$v.ccEmails.$error" class="message"> <span v-if="$v.ccEmailsVal.$error" class="message">
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }} {{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
</span> </span>
</div> </div>
<div v-if="showBcc" class="input-group-wrap"> <div v-if="showBcc" class="input-group-wrap">
<div class="input-group small" :class="{ error: $v.bccEmails.$error }"> <div class="input-group small" :class="{ error: $v.bccEmailsVal.$error }">
<label class="input-group-label"> <label class="input-group-label">
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }} {{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
</label> </label>
<div class="input-group-field"> <div class="input-group-field">
<woot-input <woot-input
v-model.trim="bccEmails" v-model.trim="$v.bccEmailsVal.$model"
type="email" type="email"
:class="{ error: $v.bccEmails.$error }" :class="{ error: $v.bccEmailsVal.$error }"
:placeholder=" :placeholder="
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER') $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
" "
@blur="$v.bccEmails.$touch" @blur="onBlur"
/> />
</div> </div>
</div> </div>
<span v-if="$v.bccEmails.$error" class="message"> <span v-if="$v.bccEmailsVal.$error" class="message">
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }} {{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
</span> </span>
</div> </div>
@@ -55,27 +55,25 @@
import { validEmailsByComma } from './helpers/emailHeadHelper'; import { validEmailsByComma } from './helpers/emailHeadHelper';
export default { export default {
props: { props: {
ccEmails: { clearMails: {
type: String, type: Boolean,
default: '', default: false,
},
bccEmails: {
type: String,
default: '',
}, },
}, },
data() { data() {
return { return {
showBcc: false, showBcc: false,
ccEmailsVal: '',
bccEmailsVal: '',
}; };
}, },
validations: { validations: {
ccEmails: { ccEmailsVal: {
hasValidEmails(value) { hasValidEmails(value) {
return validEmailsByComma(value); return validEmailsByComma(value);
}, },
}, },
bccEmails: { bccEmailsVal: {
hasValidEmails(value) { hasValidEmails(value) {
return validEmailsByComma(value); return validEmailsByComma(value);
}, },
@@ -85,7 +83,20 @@ export default {
handleAddBcc() { handleAddBcc() {
this.showBcc = true; this.showBcc = true;
}, },
onBlur() {
this.$v.$touch();
this.$emit("set-emails", { bccEmails: this.bccEmailsVal, ccEmails: this.ccEmailsVal });
},
}, },
watch: {
clearMails: function(value){
if(value) {
this.ccEmailsVal = '';
this.bccEmailsVal = '';
this.clearMails = false;
}
}
}
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,6 +1,12 @@
<template> <template>
<div class="message-text--metadata"> <div class="message-text--metadata">
<span class="time">{{ readableTime }}</span> <span class="time">{{ readableTime }}</span>
<span v-if="showSentIndicator" class="time">
<i
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
class="icon ion-checkmark"
/>
</span>
<i <i
v-if="isEmail" v-if="isEmail"
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')" v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
@@ -36,8 +42,10 @@
<script> <script>
import { MESSAGE_TYPE } from 'shared/constants/messages'; import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import inboxMixin from 'shared/mixins/inboxMixin';
export default { export default {
mixins: [inboxMixin],
props: { props: {
sender: { sender: {
type: Object, type: Object,
@@ -99,6 +107,9 @@ export default {
return `https://twitter.com/${screenName || return `https://twitter.com/${screenName ||
this.inbox.name}/status/${sourceId}`; this.inbox.name}/status/${sourceId}`;
}, },
showSentIndicator() {
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
},
}, },
methods: { methods: {
onTweetReply() { onTweetReply() {
@@ -117,6 +128,10 @@ export default {
color: var(--w-100); color: var(--w-100);
} }
} }
.icon {
color: var(--white);
}
} }
.left { .left {
@@ -127,13 +142,6 @@ export default {
} }
} }
.right {
.ion-reply,
.ion-android-open {
color: var(--white);
}
}
.message-text--metadata { .message-text--metadata {
align-items: flex-end; align-items: flex-end;
display: flex; display: flex;
@@ -192,6 +200,10 @@ export default {
.time { .time {
color: var(--s-400); color: var(--s-400);
} }
.icon {
color: var(--s-400);
}
} }
&.is-image { &.is-image {
@@ -201,4 +213,8 @@ export default {
} }
} }
} }
.delivered-icon {
margin-left: -var(--space-normal);
}
</style> </style>

View File

@@ -36,6 +36,14 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
cc: {
type: Array,
default: () => [],
},
bcc: {
type: Array,
default: () => [],
},
}, },
computed: { computed: {
toMails() { toMails() {
@@ -43,11 +51,11 @@ export default {
return to.join(', '); return to.join(', ');
}, },
ccMails() { ccMails() {
const cc = this.emailAttributes.cc || []; const cc = this.emailAttributes.cc || this.cc || [];
return cc.join(', '); return cc.join(', ');
}, },
bccMails() { bccMails() {
const bcc = this.emailAttributes.bcc || []; const bcc = this.emailAttributes.bcc || this.bcc || [];
return bcc.join(', '); return bcc.join(', ');
}, },
subject() { subject() {

View File

@@ -60,11 +60,9 @@ export default {
.text-content { .text-content {
overflow: auto; overflow: auto;
&::v-deep { ul,
ul, ol {
ol { padding-left: var(--space-two);
margin-left: var(--space-normal);
}
} }
table { table {
all: revert; all: revert;

View File

@@ -1,6 +1,7 @@
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import Button from 'dashboard/components/buttons/Button'; import Button from 'dashboard/components/buttons/Button';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
@@ -10,6 +11,7 @@ import MoreActions from '../MoreActions';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
localVue.use(VueI18n); localVue.use(VueI18n);
localVue.use(VTooltip);
localVue.component('woot-button', Button); localVue.component('woot-button', Button);
@@ -63,21 +65,9 @@ describe('MoveActions', () => {
moreActions = mount(MoreActions, { store, localVue, i18n: i18nConfig }); moreActions = mount(MoreActions, { store, localVue, i18n: i18nConfig });
}); });
it('opens the menu when user clicks "more"', async () => {
expect(moreActions.find('.dropdown-pane').exists()).toBe(false);
await moreActions.find('.more--button').trigger('click');
expect(moreActions.find('.dropdown-pane').exists()).toBe(true);
});
describe('muting discussion', () => { describe('muting discussion', () => {
it('triggers "muteConversation"', async () => { it('triggers "muteConversation"', async () => {
await moreActions.find('.more--button').trigger('click'); await moreActions.find('button:first-child').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(muteConversation).toBeCalledWith( expect(muteConversation).toBeCalledWith(
expect.any(Object), expect.any(Object),
@@ -87,11 +77,7 @@ describe('MoveActions', () => {
}); });
it('shows alert', async () => { it('shows alert', async () => {
await moreActions.find('.more--button').trigger('click'); await moreActions.find('button:first-child').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(window.bus.$emit).toBeCalledWith( expect(window.bus.$emit).toBeCalledWith(
'newToastMessage', 'newToastMessage',
@@ -106,11 +92,7 @@ describe('MoveActions', () => {
}); });
it('triggers "unmuteConversation"', async () => { it('triggers "unmuteConversation"', async () => {
await moreActions.find('.more--button').trigger('click'); await moreActions.find('button:first-child').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(unmuteConversation).toBeCalledWith( expect(unmuteConversation).toBeCalledWith(
expect.any(Object), expect.any(Object),
@@ -120,11 +102,7 @@ describe('MoveActions', () => {
}); });
it('shows alert', async () => { it('shows alert', async () => {
await moreActions.find('.more--button').trigger('click'); await moreActions.find('button:first-child').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(window.bus.$emit).toBeCalledWith( expect(window.bus.$emit).toBeCalledWith(
'newToastMessage', 'newToastMessage',

View File

@@ -5,6 +5,7 @@
:value="value" :value="value"
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:readonly="readonly"
@input="onChange" @input="onChange"
@blur="onBlur" @blur="onBlur"
/> />
@@ -42,6 +43,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
readonly: {
type: Boolean,
deafaut: false,
},
}, },
methods: { methods: {
onChange(e) { onChange(e) {

View File

@@ -10,6 +10,7 @@ export default {
RESOLVED: 'resolved', RESOLVED: 'resolved',
PENDING: 'pending', PENDING: 'pending',
SNOOZED: 'snoozed', SNOOZED: 'snoozed',
ALL: 'all',
}, },
}; };
export const DEFAULT_REDIRECT_URL = '/app/'; export const DEFAULT_REDIRECT_URL = '/app/';

View File

@@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.typing_off': this.onTypingOff, 'conversation.typing_off': this.onTypingOff,
'conversation.contact_changed': this.onConversationContactChange, 'conversation.contact_changed': this.onConversationContactChange,
'presence.update': this.onPresenceUpdate, 'presence.update': this.onPresenceUpdate,
'contact.deleted': this.onContactDelete,
}; };
} }
@@ -33,7 +34,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onPresenceUpdate = data => { onPresenceUpdate = data => {
this.app.$store.dispatch('contacts/updatePresence', data.contacts); this.app.$store.dispatch('contacts/updatePresence', data.contacts);
this.app.$store.dispatch('agents/updatePresence', data.users); this.app.$store.dispatch('agents/updatePresence', data.users);
this.app.$store.dispatch('setCurrentUserAvailabilityStatus', data.users); this.app.$store.dispatch('setCurrentUserAvailability', data.users);
}; };
onConversationContactChange = payload => { onConversationContactChange = payload => {
@@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
fetchConversationStats = () => { fetchConversationStats = () => {
bus.$emit('fetch_conversation_stats'); bus.$emit('fetch_conversation_stats');
}; };
onContactDelete = data => {
this.app.$store.dispatch(
'contacts/deleteContactThroughConversations',
data.id
);
this.fetchConversationStats();
};
} }
export default { export default {

View File

@@ -0,0 +1,6 @@
export const downloadCsvFile = (fileName, fileContent) => {
const link = document.createElement('a');
link.download = fileName;
link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent);
link.click();
};

View File

@@ -16,6 +16,9 @@ export const getInboxClassByType = (type, phoneNumber) => {
? 'ion-social-whatsapp-outline' ? 'ion-social-whatsapp-outline'
: 'ion-android-textsms'; : 'ion-android-textsms';
case INBOX_TYPES.WHATSAPP:
return 'ion-social-whatsapp-outline';
case INBOX_TYPES.API: case INBOX_TYPES.API:
return 'ion-cloud'; return 'ion-cloud';

View File

@@ -0,0 +1,21 @@
import { downloadCsvFile } from '../downloadCsvFile';
const fileName = 'test.csv';
const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
Pranav,36,114,28411`;
describe('#downloadCsvFile', () => {
it('should download the csv file', () => {
const link = {
click: jest.fn(),
};
jest.spyOn(document, 'createElement').mockImplementation(() => link);
downloadCsvFile(fileName, fileData);
expect(link.download).toEqual(fileName);
expect(link.href).toEqual(
`data:text/csv;charset=utf-8,${encodeURI(fileData)}`
);
expect(link.click).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,245 +1,13 @@
import { frontendURL } from '../helper/URLHelper'; import common from './sidebarItems/common';
import contacts from './sidebarItems/contacts';
import reports from './sidebarItems/reports';
import campaigns from './sidebarItems/campaigns';
import settings from './sidebarItems/settings';
export const getSidebarItems = accountId => ({ export const getSidebarItems = accountId => ({
common: { common: common(accountId),
routes: [ contacts: contacts(accountId),
'home', reports: reports(accountId),
'inbox_dashboard', campaigns: campaigns(accountId),
'inbox_conversation', settings: settings(accountId),
'conversation_through_inbox',
'notifications_dashboard',
'profile_settings',
'profile_settings_index',
'label_conversations',
'conversations_through_label',
'team_conversations',
'conversations_through_team',
'notifications_index',
],
menuItems: {
assignedToMe: {
icon: 'ion-chatbox-working',
label: 'CONVERSATIONS',
hasSubMenu: false,
key: '',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
contacts: {
icon: 'ion-person',
label: 'CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
},
notifications: {
icon: 'ion-ios-bell',
label: 'NOTIFICATIONS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/notifications`),
toStateName: 'notifications_dashboard',
},
report: {
icon: 'ion-arrow-graph-up-right',
label: 'REPORTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports',
},
campaigns: {
icon: 'ion-speakerphone',
label: 'CAMPAIGNS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'settings_account_campaigns',
},
settings: {
icon: 'ion-settings',
label: 'SETTINGS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings`),
toStateName: 'settings_home',
},
},
},
contacts: {
routes: [
'contacts_dashboard',
'contacts_dashboard_manage',
'contacts_labels_dashboard',
],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
contacts: {
icon: 'ion-person',
label: 'ALL_CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
},
},
},
reports: {
routes: ['settings_account_reports', 'csat_reports'],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
reportOverview: {
icon: 'ion-arrow-graph-up-right',
label: 'REPORTS_OVERVIEW',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/overview`),
toStateName: 'settings_account_reports',
},
csatReports: {
icon: 'ion-happy',
label: 'CSAT',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/csat`),
toStateName: 'csat_reports',
},
},
},
campaigns: {
routes: ['settings_account_campaigns', 'one_off'],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
ongoingCampaigns: {
icon: 'ion-arrow-swap',
label: 'ONGOING',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
toStateName: 'settings_account_campaigns',
},
onOffCampaigns: {
icon: 'ion-radio-waves',
label: 'ONE_OFF',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
toStateName: 'one_off',
},
},
},
settings: {
routes: [
'agent_list',
'canned_list',
'labels_list',
'settings_inbox',
'attributes_list',
'settings_inbox_new',
'settings_inbox_list',
'settings_inbox_show',
'settings_inboxes_page_channel',
'settings_inboxes_add_agents',
'settings_inbox_finish',
'settings_integrations',
'settings_integrations_webhook',
'settings_integrations_integration',
'settings_applications',
'settings_applications_webhook',
'settings_applications_integration',
'general_settings',
'general_settings_index',
'settings_teams_list',
'settings_teams_new',
'settings_teams_add_agents',
'settings_teams_finish',
'settings_teams_edit',
'settings_teams_edit_members',
'settings_teams_edit_finish',
],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'HOME',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL(`accounts/${accountId}/dashboard`),
},
agents: {
icon: 'ion-person-stalker',
label: 'AGENTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list',
},
teams: {
icon: 'ion-ios-people',
label: 'TEAMS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list',
},
inboxes: {
icon: 'ion-archive',
label: 'INBOXES',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
},
labels: {
icon: 'ion-pricetags',
label: 'LABELS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
},
attributes: {
icon: 'ion-code',
label: 'ATTRIBUTES',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/attributes/list`),
toStateName: 'attributes_list',
},
cannedResponses: {
icon: 'ion-chatbox-working',
label: 'CANNED_RESPONSES',
hasSubMenu: false,
toState: frontendURL(
`accounts/${accountId}/settings/canned-response/list`
),
toStateName: 'canned_list',
},
settings_integrations: {
icon: 'ion-flash',
label: 'INTEGRATIONS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
},
settings_applications: {
icon: 'ion-asterisk',
label: 'APPLICATIONS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
},
general_settings_index: {
icon: 'ion-gear-a',
label: 'ACCOUNT_SETTINGS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/general`),
toStateName: 'general_settings_index',
},
},
},
}); });

View File

@@ -54,6 +54,7 @@
"ERROR": "الوقت على الصفحة مطلوب" "ERROR": "الوقت على الصفحة مطلوب"
}, },
"ENABLED": "تفعيل الحملة", "ENABLED": "تفعيل الحملة",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "إضافة حملة" "SUBMIT": "إضافة حملة"
}, },
"API": { "API": {

View File

@@ -10,6 +10,7 @@
"SEARCH": { "SEARCH": {
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .." "INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
}, },
"FILTER_ALL": "الكل",
"STATUS_TABS": [ "STATUS_TABS": [
{ {
"NAME": "فتح", "NAME": "فتح",
@@ -48,11 +49,11 @@
}, },
{ {
"TEXT": "معلق", "TEXT": "معلق",
"VALUE": "pending" "VALUE": "معلق"
}, },
{ {
"TEXT": "غفوة", "TEXT": "غفوة",
"VALUE": "snoozed" "VALUE": "غفوة"
} }
], ],
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -85,6 +86,8 @@
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر", "VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
"REPLY_TO_TWEET": "الرد على هذه التغريدة", "REPLY_TO_TWEET": "الرد على هذه التغريدة",
"NO_MESSAGES": "لا توجد رسائل", "NO_MESSAGES": "لا توجد رسائل",
"NO_CONTENT": "لم يتم العثور على محتوى" "NO_CONTENT": "لم يتم العثور على محتوى",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
"SHOW_QUOTED_TEXT": "Show Quoted Text"
} }
} }

View File

@@ -32,6 +32,8 @@
"NO_RESULT": "لم يتم العثور على تصنيفات" "NO_RESULT": "لم يتم العثور على تصنيفات"
} }
}, },
"MERGE_CONTACT": "Merge contact",
"CONTACT_ACTIONS": "Contact actions",
"MUTE_CONTACT": "كتم المحادثة", "MUTE_CONTACT": "كتم المحادثة",
"UNMUTE_CONTACT": "إلغاء كتم المحادثة", "UNMUTE_CONTACT": "إلغاء كتم المحادثة",
"MUTED_SUCCESS": "تم كتم هذه المحادثة لمدة 6 ساعات", "MUTED_SUCCESS": "تم كتم هذه المحادثة لمدة 6 ساعات",
@@ -54,6 +56,35 @@
"TITLE": "إنشاء جهة اتصال جديدة", "TITLE": "إنشاء جهة اتصال جديدة",
"DESC": "إضافة معلومات أساسية حول جهة الاتصال." "DESC": "إضافة معلومات أساسية حول جهة الاتصال."
}, },
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import",
"TITLE": "Import Contacts",
"DESC": "Import contacts through a CSV file.",
"DOWNLOAD_LABEL": "Download a sample csv.",
"FORM": {
"LABEL": "CSV File",
"SUBMIT": "Import",
"CANCEL": "إلغاء"
},
"SUCCESS_MESSAGE": "Contacts saved successfully",
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
},
"DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact",
"TITLE": "Delete contact",
"DESC": "Delete contact details",
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من الحذف ",
"PLACE_HOLDER": "Please type {contactName} to confirm",
"YES": "نعم، احذف ",
"NO": "لا، احتفظ "
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
}
},
"CONTACT_FORM": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "إرسال", "SUBMIT": "إرسال",
@@ -213,17 +244,19 @@
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
"TITLE": "دمج جهة الاتصال", "TITLE": "دمج جهة الاتصال",
"DESCRIPTION": "دمج جهة الاتصال مفيد عندما يكون لديك مدخلات مكررة لنفس جهة الاتصال. عملية الدمج تأخذ جهة اتصال رئيسية وتدمجها بجهة الاتصال المكررة. بعد الدمج، ستبقى جميع التفاصيل في جهة الاتصال الرئيسية كما هي. إذا لم يكن لدى جهة الاتصال الرئيسية حقل ، فسيتم استخدام القيمة من جهة الاتصال المكررة بعد الدمج. إذا حدث تضارب بالبيانات، ستبقى الحقول في جهة الاتصال الأساسية غير متأثرة، ولكن الحقول من جهة الاتصال الثانوية سيتم نسخها إلى السمات المخصصة في جهة الاتصال الرئيسية.", "DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": { "PRIMARY": {
"TITLE": "جهة الاتصال الرئيسية" "TITLE": "جهة الاتصال الرئيسية",
"HELP_LABEL": "To be kept"
}, },
"CHILD": { "CHILD": {
"TITLE": "دمج جهة الإتصال", "TITLE": "دمج جهة الإتصال",
"PLACEHOLDER": "اختر جهة اتصال" "PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "ملخص", "TITLE": "ملخص",
"DELETE_WARNING": "الاتصال بـ <strong>%{childContactName}</strong>سيتم حذفه.", "DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{childContactName}</strong> إلى <strong>%{primaryContactName}</strong>." "ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{childContactName}</strong> إلى <strong>%{primaryContactName}</strong>."
}, },
"SEARCH": { "SEARCH": {
@@ -236,7 +269,7 @@
"ERROR": "حدد جهة اتصال فرعية للدمج" "ERROR": "حدد جهة اتصال فرعية للدمج"
}, },
"SUCCESS_MESSAGE": "تم دمج جهة الاتصال بنجاح", "SUCCESS_MESSAGE": "تم دمج جهة الاتصال بنجاح",
"ERROR_MESSAGE": "تعذر دمج جهات الاتصال ، حاول مرة أخرى!" "ERROR_MESSAGE": "Could not merge contacts, try again!"
} }
} }
} }

View File

@@ -39,7 +39,10 @@
"OPEN_ACTION": "فتح", "OPEN_ACTION": "فتح",
"OPEN": "المزيد", "OPEN": "المزيد",
"CLOSE": "أغلق", "CLOSE": "أغلق",
"DETAILS": "التفاصيل" "DETAILS": "التفاصيل",
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
}, },
"RESOLVE_DROPDOWN": { "RESOLVE_DROPDOWN": {
"MARK_PENDING": "تحديد كمعلق", "MARK_PENDING": "تحديد كمعلق",
@@ -84,6 +87,7 @@
"CHANGE_AGENT": "تم تغيير الموظف الذي تم إحالة المحادثة إليه", "CHANGE_AGENT": "تم تغيير الموظف الذي تم إحالة المحادثة إليه",
"CHANGE_TEAM": "تم تغيير فريق المحادثة", "CHANGE_TEAM": "تم تغيير فريق المحادثة",
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_FILE_UPLOAD_SIZE}", "FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_FILE_UPLOAD_SIZE}",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "أرسلت بواسطة:", "SENT_BY": "أرسلت بواسطة:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "اختر وكيل", "SELECT_AGENT": "اختر وكيل",

View File

@@ -71,5 +71,13 @@
"assigned_conversation_new_message": "رسالة جديدة", "assigned_conversation_new_message": "رسالة جديدة",
"conversation_mention": "إشارة" "conversation_mention": "إشارة"
} }
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "Disconnected from Chatwoot"
},
"BUTTON": {
"REFRESH": "Refresh"
}
} }
} }

View File

@@ -56,6 +56,11 @@
"CHANNEL_AVATAR": { "CHANNEL_AVATAR": {
"LABEL": "الصورة الرمزية للقناة" "LABEL": "الصورة الرمزية للقناة"
}, },
"CHANNEL_WEBHOOK_URL": {
"LABEL": "رابط Webhook",
"PLACEHOLDER": "Enter your Webhook URL",
"ERROR": "الرجاء إدخال عنوان URL صالح"
},
"CHANNEL_DOMAIN": { "CHANNEL_DOMAIN": {
"LABEL": "نطاق الموقع", "LABEL": "نطاق الموقع",
"PLACEHOLDER": "أدخل نطاق موقعك الإلكتروني (مثال: acme.com)" "PLACEHOLDER": "أدخل نطاق موقعك الإلكتروني (مثال: acme.com)"
@@ -92,8 +97,8 @@
"SUBMIT_BUTTON": "إنشاء قناة تواصل" "SUBMIT_BUTTON": "إنشاء قناة تواصل"
}, },
"TWILIO": { "TWILIO": {
"TITLE": "قناة Twilio SMS/WhatsApp", "TITLE": "Twilio SMS/WhatsApp Channel",
"DESC": "قم بإضافة قناة Twilio لتمكن عملائك من التواصل معك عبر الرسائل القصيرة SMS أو عبر واتساب.", "DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
"ACCOUNT_SID": { "ACCOUNT_SID": {
"LABEL": "معرف حساب Twilio (يعرف أيضاً بـ Account SID)", "LABEL": "معرف حساب Twilio (يعرف أيضاً بـ Account SID)",
"PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)", "PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)",
@@ -109,8 +114,8 @@
"ERROR": "هذا الحقل مطلوب" "ERROR": "هذا الحقل مطلوب"
}, },
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "اسم القناة", "LABEL": "اسم صندوق الوارد لقناة التواصل",
"PLACEHOLDER": "الرجاء إدخال اسم القناة", "PLACEHOLDER": "Please enter a inbox name",
"ERROR": "هذا الحقل مطلوب" "ERROR": "هذا الحقل مطلوب"
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
@@ -132,8 +137,34 @@
"DESC": "ابدأ في دعم عملائك عبر الرسائل القصيرة بإستخدام Twilio." "DESC": "ابدأ في دعم عملائك عبر الرسائل القصيرة بإستخدام Twilio."
}, },
"WHATSAPP": { "WHATSAPP": {
"TITLE": "قناة Whatsapp عبر Twilio", "TITLE": "WhatsApp Channel",
"DESC": "ابدأ في دعم عملائك عبر الواتساب بإستخدام Twilio." "DESC": "Start supporting your customers via WhatsApp.",
"PROVIDERS": {
"LABEL": "API Provider",
"TWILIO": "Twilio",
"360_DIALOG": "360Dialog"
},
"INBOX_NAME": {
"LABEL": "اسم صندوق الوارد لقناة التواصل",
"PLACEHOLDER": "Please enter an inbox name",
"ERROR": "هذا الحقل مطلوب"
},
"PHONE_NUMBER": {
"LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
},
"API_KEY": {
"LABEL": "API key",
"SUBTITLE": "Configure the WhatsApp API key.",
"PLACEHOLDER": "API key",
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
"ERROR": "Please enter a valid value."
},
"SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
}
}, },
"API_CHANNEL": { "API_CHANNEL": {
"TITLE": "قناة API", "TITLE": "قناة API",
@@ -195,6 +226,10 @@
"SUBMIT_BUTTON": "Create LINE Channel", "SUBMIT_BUTTON": "Create LINE Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the LINE channel" "ERROR_MESSAGE": "We were not able to save the LINE channel"
},
"API_CALLBACK": {
"TITLE": "عنوان Callback URL",
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
} }
}, },
"TELEGRAM_CHANNEL": { "TELEGRAM_CHANNEL": {
@@ -212,7 +247,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "اختر قناة", "TITLE": "اختر قناة",
"DESC": "شاتوت يدعم أداة الدردشة المباشرة، صفحة الفيسبوك، ملف تويتر الشخصي، واتسب، البريد الإلكتروني وما إلى ذلك، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. حدد قناة واحدة من الخيارات أدناه للمتابعة." "DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "موظف الدعم", "TITLE": "موظف الدعم",
@@ -266,6 +301,9 @@
"ENABLE_CSAT": { "ENABLE_CSAT": {
"ENABLED": "مفعل", "ENABLED": "مفعل",
"DISABLED": "معطّل" "DISABLED": "معطّل"
},
"ENABLE_HMAC": {
"LABEL": "Enable"
} }
}, },
"DELETE": { "DELETE": {
@@ -315,6 +353,8 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.", "AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
"HMAC_VERIFICATION": "التحقق من هوية المستخدم", "HMAC_VERIFICATION": "التحقق من هوية المستخدم",
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.", "HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
"INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email", "FORWARD_EMAIL_TITLE": "Forward to Email",
@@ -350,7 +390,7 @@
"TIMEZONE_LABEL": "اختر المنطقة الزمنية", "TIMEZONE_LABEL": "اختر المنطقة الزمنية",
"UPDATE": "تحديث إعدادات ساعات العمل", "UPDATE": "تحديث إعدادات ساعات العمل",
"TOGGLE_AVAILABILITY": "تمكين توافر العمل لهذا البريد الوارد", "TOGGLE_AVAILABILITY": "تمكين توافر العمل لهذا البريد الوارد",
"UNAVAILABLE_MESSAGE_LABEL": "رسالة غير متاح للزائرين", "UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
"UNAVAILABLE_MESSAGE_DEFAULT": "نحن غير متوفرين في هذه اللحظة. اترك رسالة سنرد عليها بمجرد عودتنا.", "UNAVAILABLE_MESSAGE_DEFAULT": "نحن غير متوفرين في هذه اللحظة. اترك رسالة سنرد عليها بمجرد عودتنا.",
"TOGGLE_HELP": "تمكين توفر العمل سيظهر الساعات المتاحة على أداة الدردشة المباشرة حتى لو كان جميع الوكلاء غير متصلين بالإنترنت. خارج الساعات المتاحة يمكن تحذير الزوار برسالة ونموذج ما قبل الدردشة.", "TOGGLE_HELP": "تمكين توفر العمل سيظهر الساعات المتاحة على أداة الدردشة المباشرة حتى لو كان جميع الوكلاء غير متصلين بالإنترنت. خارج الساعات المتاحة يمكن تحذير الزوار برسالة ونموذج ما قبل الدردشة.",
"DAY": { "DAY": {

View File

@@ -61,6 +61,258 @@
"PLACEHOLDER": "اختر نطاق المدة" "PLACEHOLDER": "اختر نطاق المدة"
} }
}, },
"AGENT_REPORTS": {
"HEADER": "Agents Overview",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_AGENT_REPORTS": "تنزيل تقارير الوكيل",
"FILTER_DROPDOWN_LABEL": "اختر وكيل",
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
"DESC": "(الإجمالي)"
},
"INCOMING_MESSAGES": {
"NAME": "الرسائل الواردة",
"DESC": "(الإجمالي)"
},
"OUTGOING_MESSAGES": {
"NAME": "الرسائل الصادرة",
"DESC": "(الإجمالي)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
"DESC": "(الإجمالي)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "آخر 7 أيام"
},
{
"id": 1,
"name": "آخر 30 يوماً"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "العام الماضي"
},
{
"id": 5,
"name": "تحديد نطاق المدة"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "تطبيق",
"PLACEHOLDER": "اختر نطاق المدة"
}
},
"LABEL_REPORTS": {
"HEADER": "Labels Overview",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
"FILTER_DROPDOWN_LABEL": "Select Label",
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
"DESC": "(الإجمالي)"
},
"INCOMING_MESSAGES": {
"NAME": "الرسائل الواردة",
"DESC": "(الإجمالي)"
},
"OUTGOING_MESSAGES": {
"NAME": "الرسائل الصادرة",
"DESC": "(الإجمالي)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
"DESC": "(الإجمالي)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "آخر 7 أيام"
},
{
"id": 1,
"name": "آخر 30 يوماً"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "العام الماضي"
},
{
"id": 5,
"name": "تحديد نطاق المدة"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "تطبيق",
"PLACEHOLDER": "اختر نطاق المدة"
}
},
"INBOX_REPORTS": {
"HEADER": "Inbox Overview",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
"FILTER_DROPDOWN_LABEL": "اختر صندوق الوارد",
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
"DESC": "(الإجمالي)"
},
"INCOMING_MESSAGES": {
"NAME": "الرسائل الواردة",
"DESC": "(الإجمالي)"
},
"OUTGOING_MESSAGES": {
"NAME": "الرسائل الصادرة",
"DESC": "(الإجمالي)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
"DESC": "(الإجمالي)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "آخر 7 أيام"
},
{
"id": 1,
"name": "آخر 30 يوماً"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "العام الماضي"
},
{
"id": 5,
"name": "تحديد نطاق المدة"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "تطبيق",
"PLACEHOLDER": "اختر نطاق المدة"
}
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"METRICS": {
"CONVERSATIONS": {
"NAME": "المحادثات",
"DESC": "(الإجمالي)"
},
"INCOMING_MESSAGES": {
"NAME": "الرسائل الواردة",
"DESC": "(الإجمالي)"
},
"OUTGOING_MESSAGES": {
"NAME": "الرسائل الصادرة",
"DESC": "(الإجمالي)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "وقت الاستجابة الأولى",
"DESC": "(متوسط)"
},
"RESOLUTION_TIME": {
"NAME": "وقت إغلاق المحادثات",
"DESC": "(متوسط)"
},
"RESOLUTION_COUNT": {
"NAME": "عدد مرات الإغلاق",
"DESC": "(الإجمالي)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "آخر 7 أيام"
},
{
"id": 1,
"name": "آخر 30 يوماً"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "العام الماضي"
},
{
"id": 5,
"name": "تحديد نطاق المدة"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "تطبيق",
"PLACEHOLDER": "اختر نطاق المدة"
}
},
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "تقارير CSAT", "HEADER": "تقارير CSAT",
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.", "NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
@@ -87,4 +339,4 @@
} }
} }
} }
} }

View File

@@ -150,7 +150,11 @@
"CSAT": "CSAT", "CSAT": "CSAT",
"CAMPAIGNS": "الحملات", "CAMPAIGNS": "الحملات",
"ONGOING": "جارية", "ONGOING": "جارية",
"ONE_OFF": "إيقاف واحد" "ONE_OFF": "إيقاف واحد",
"REPORTS_AGENT": "موظف الدعم",
"REPORTS_LABEL": "الوسوم",
"REPORTS_INBOX": "صندوق الوارد",
"REPORTS_TEAM": "Team"
}, },
"CREATE_ACCOUNT": { "CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.", "NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",

View File

@@ -54,6 +54,7 @@
"ERROR": "Time on page is required" "ERROR": "Time on page is required"
}, },
"ENABLED": "Enable campaign", "ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "Add Campaign" "SUBMIT": "Add Campaign"
}, },
"API": { "API": {

View File

@@ -10,6 +10,7 @@
"SEARCH": { "SEARCH": {
"INPUT": "Cerca persones, xats, respostes desades .." "INPUT": "Cerca persones, xats, respostes desades .."
}, },
"FILTER_ALL": "Totes",
"STATUS_TABS": [ "STATUS_TABS": [
{ {
"NAME": "Obrir", "NAME": "Obrir",
@@ -85,6 +86,8 @@
"VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter", "VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter",
"REPLY_TO_TWEET": "Respon a aquest tuit", "REPLY_TO_TWEET": "Respon a aquest tuit",
"NO_MESSAGES": "Cap Missatge", "NO_MESSAGES": "Cap Missatge",
"NO_CONTENT": "No content available" "NO_CONTENT": "No content available",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
"SHOW_QUOTED_TEXT": "Show Quoted Text"
} }
} }

View File

@@ -32,6 +32,8 @@
"NO_RESULT": "No labels found" "NO_RESULT": "No labels found"
} }
}, },
"MERGE_CONTACT": "Merge contact",
"CONTACT_ACTIONS": "Contact actions",
"MUTE_CONTACT": "Silencia la conversa", "MUTE_CONTACT": "Silencia la conversa",
"UNMUTE_CONTACT": "Desactiva el silenci de la conversa", "UNMUTE_CONTACT": "Desactiva el silenci de la conversa",
"MUTED_SUCCESS": "Aquesta conversa s'ha silenciat durant 6 hores", "MUTED_SUCCESS": "Aquesta conversa s'ha silenciat durant 6 hores",
@@ -54,6 +56,35 @@
"TITLE": "Crear un nou contacte", "TITLE": "Crear un nou contacte",
"DESC": "Afegir informació bàsica sobre el contacte." "DESC": "Afegir informació bàsica sobre el contacte."
}, },
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import",
"TITLE": "Import Contacts",
"DESC": "Import contacts through a CSV file.",
"DOWNLOAD_LABEL": "Download a sample csv.",
"FORM": {
"LABEL": "CSV File",
"SUBMIT": "Import",
"CANCEL": "Cancel·la"
},
"SUCCESS_MESSAGE": "Contacts saved successfully",
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
},
"DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact",
"TITLE": "Delete contact",
"DESC": "Delete contact details",
"CONFIRM": {
"TITLE": "Confirma l'esborrat",
"MESSAGE": "N'estas segur? ",
"PLACE_HOLDER": "Please type {contactName} to confirm",
"YES": "Si, esborra ",
"NO": "No, segueix "
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
}
},
"CONTACT_FORM": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "Envia", "SUBMIT": "Envia",
@@ -213,17 +244,19 @@
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
"TITLE": "Merge contacts", "TITLE": "Merge contacts",
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.", "DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": { "PRIMARY": {
"TITLE": "Primary contact" "TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
}, },
"CHILD": { "CHILD": {
"TITLE": "Contact to merge", "TITLE": "Contact to merge",
"PLACEHOLDER": "Choose a contact" "PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "Summary", "TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.", "DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>." "ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
}, },
"SEARCH": { "SEARCH": {
@@ -236,7 +269,7 @@
"ERROR": "Select a child contact to merge" "ERROR": "Select a child contact to merge"
}, },
"SUCCESS_MESSAGE": "Contact merged successfully", "SUCCESS_MESSAGE": "Contact merged successfully",
"ERROR_MESSAGE": "Could not merge contcts, try again!" "ERROR_MESSAGE": "Could not merge contacts, try again!"
} }
} }
} }

View File

@@ -39,7 +39,10 @@
"OPEN_ACTION": "Obrir", "OPEN_ACTION": "Obrir",
"OPEN": "Més", "OPEN": "Més",
"CLOSE": "Tanca", "CLOSE": "Tanca",
"DETAILS": "detalls" "DETAILS": "detalls",
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
}, },
"RESOLVE_DROPDOWN": { "RESOLVE_DROPDOWN": {
"MARK_PENDING": "Mark as pending", "MARK_PENDING": "Mark as pending",
@@ -84,6 +87,7 @@
"CHANGE_AGENT": "Assignació de la conversa canviat", "CHANGE_AGENT": "Assignació de la conversa canviat",
"CHANGE_TEAM": "Conversation team changed", "CHANGE_TEAM": "Conversation team changed",
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Enviat per:", "SENT_BY": "Enviat per:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Seleccionar Agent", "SELECT_AGENT": "Seleccionar Agent",

View File

@@ -71,5 +71,13 @@
"assigned_conversation_new_message": "Missatge Nou", "assigned_conversation_new_message": "Missatge Nou",
"conversation_mention": "Menció" "conversation_mention": "Menció"
} }
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "Disconnected from Chatwoot"
},
"BUTTON": {
"REFRESH": "Refresh"
}
} }
} }

View File

@@ -56,6 +56,11 @@
"CHANNEL_AVATAR": { "CHANNEL_AVATAR": {
"LABEL": "Avatar del canal" "LABEL": "Avatar del canal"
}, },
"CHANNEL_WEBHOOK_URL": {
"LABEL": "URL del webhook",
"PLACEHOLDER": "Enter your Webhook URL",
"ERROR": "Introduïu una URL vàlid"
},
"CHANNEL_DOMAIN": { "CHANNEL_DOMAIN": {
"LABEL": "Domini del lloc web", "LABEL": "Domini del lloc web",
"PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)" "PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)"
@@ -92,8 +97,8 @@
"SUBMIT_BUTTON": "Crea la safata entrada" "SUBMIT_BUTTON": "Crea la safata entrada"
}, },
"TWILIO": { "TWILIO": {
"TITLE": "Canal Twilio SMS", "TITLE": "Twilio SMS/WhatsApp Channel",
"DESC": "Integra Twilio i comença a donar suport als teus clients mitjançant SMS.", "DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
"ACCOUNT_SID": { "ACCOUNT_SID": {
"LABEL": "Compte SID", "LABEL": "Compte SID",
"PLACEHOLDER": "Introduïu el vostre compte Twilio SID", "PLACEHOLDER": "Introduïu el vostre compte Twilio SID",
@@ -109,8 +114,8 @@
"ERROR": "Aquest camp és obligatori" "ERROR": "Aquest camp és obligatori"
}, },
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Nom del canal", "LABEL": "Nom de la safata d'entrada",
"PLACEHOLDER": "Introduïu el nom del canal", "PLACEHOLDER": "Please enter a inbox name",
"ERROR": "Aquest camp és obligatori" "ERROR": "Aquest camp és obligatori"
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
@@ -132,8 +137,34 @@
"DESC": "Start supporting your customers via SMS with Twilio integration." "DESC": "Start supporting your customers via SMS with Twilio integration."
}, },
"WHATSAPP": { "WHATSAPP": {
"TITLE": "Whatsapp Channel via Twilio", "TITLE": "WhatsApp Channel",
"DESC": "Start supporting your customers via Whatsapp with Twilio integration." "DESC": "Start supporting your customers via WhatsApp.",
"PROVIDERS": {
"LABEL": "API Provider",
"TWILIO": "Twilio",
"360_DIALOG": "360Dialog"
},
"INBOX_NAME": {
"LABEL": "Nom de la safata d'entrada",
"PLACEHOLDER": "Please enter an inbox name",
"ERROR": "Aquest camp és obligatori"
},
"PHONE_NUMBER": {
"LABEL": "Número de telèfon",
"PLACEHOLDER": "Introduïu el número de telèfon des del qual serà enviat el missatge.",
"ERROR": "Introduïu un valor vàlid. El número de telèfon hauria de començar amb el signe `+`."
},
"API_KEY": {
"LABEL": "API key",
"SUBTITLE": "Configure the WhatsApp API key.",
"PLACEHOLDER": "API key",
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
"ERROR": "Please enter a valid value."
},
"SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
}
}, },
"API_CHANNEL": { "API_CHANNEL": {
"TITLE": "Canal de l'API", "TITLE": "Canal de l'API",
@@ -195,6 +226,10 @@
"SUBMIT_BUTTON": "Create LINE Channel", "SUBMIT_BUTTON": "Create LINE Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the LINE channel" "ERROR_MESSAGE": "We were not able to save the LINE channel"
},
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
} }
}, },
"TELEGRAM_CHANNEL": { "TELEGRAM_CHANNEL": {
@@ -212,7 +247,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Choose a channel", "TITLE": "Choose a channel",
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed." "DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Agents", "TITLE": "Agents",
@@ -266,6 +301,9 @@
"ENABLE_CSAT": { "ENABLE_CSAT": {
"ENABLED": "Habilita", "ENABLED": "Habilita",
"DISABLED": "Inhabilita" "DISABLED": "Inhabilita"
},
"ENABLE_HMAC": {
"LABEL": "Enable"
} }
}, },
"DELETE": { "DELETE": {
@@ -315,6 +353,8 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses", "AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari", "HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.", "HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
"INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email", "FORWARD_EMAIL_TITLE": "Forward to Email",
@@ -350,7 +390,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors", "UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.", "UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.", "TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
"DAY": { "DAY": {

View File

@@ -61,6 +61,258 @@
"PLACEHOLDER": "Select date range" "PLACEHOLDER": "Select date range"
} }
}, },
"AGENT_REPORTS": {
"HEADER": "Agents Overview",
"LOADING_CHART": "S'estan carregant dades del gràfic...",
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
"DOWNLOAD_AGENT_REPORTS": "Descarregar Informes d'Agent",
"FILTER_DROPDOWN_LABEL": "Seleccionar Agent",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Converses",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "Missatges d'entrada",
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "Missatges de sortida",
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Primer temps de resposta",
"DESC": "( Promig )"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
"DESC": "( Total )"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Últims 7 dies"
},
{
"id": 1,
"name": "Últims 30 dies"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"LABEL_REPORTS": {
"HEADER": "Labels Overview",
"LOADING_CHART": "S'estan carregant dades del gràfic...",
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
"FILTER_DROPDOWN_LABEL": "Select Label",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Converses",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "Missatges d'entrada",
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "Missatges de sortida",
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Primer temps de resposta",
"DESC": "( Promig )"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
"DESC": "( Total )"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Últims 7 dies"
},
{
"id": 1,
"name": "Últims 30 dies"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"INBOX_REPORTS": {
"HEADER": "Inbox Overview",
"LOADING_CHART": "S'estan carregant dades del gràfic...",
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
"FILTER_DROPDOWN_LABEL": "Select Inbox",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Converses",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "Missatges d'entrada",
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "Missatges de sortida",
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Primer temps de resposta",
"DESC": "( Promig )"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
"DESC": "( Total )"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Últims 7 dies"
},
{
"id": 1,
"name": "Últims 30 dies"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"LOADING_CHART": "S'estan carregant dades del gràfic...",
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Converses",
"DESC": "( Total )"
},
"INCOMING_MESSAGES": {
"NAME": "Missatges d'entrada",
"DESC": "( Total )"
},
"OUTGOING_MESSAGES": {
"NAME": "Missatges de sortida",
"DESC": "( Total )"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Primer temps de resposta",
"DESC": "( Promig )"
},
"RESOLUTION_TIME": {
"NAME": "Temps de resolució",
"DESC": "( Promig )"
},
"RESOLUTION_COUNT": {
"NAME": "Total de resolucions",
"DESC": "( Total )"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Últims 7 dies"
},
{
"id": 1,
"name": "Últims 30 dies"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "CSAT Reports", "HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.", "NO_RECORDS": "There are no CSAT survey responses available.",
@@ -87,4 +339,4 @@
} }
} }
} }
} }

View File

@@ -150,7 +150,11 @@
"CSAT": "CSAT", "CSAT": "CSAT",
"CAMPAIGNS": "Campaigns", "CAMPAIGNS": "Campaigns",
"ONGOING": "Ongoing", "ONGOING": "Ongoing",
"ONE_OFF": "One off" "ONE_OFF": "One off",
"REPORTS_AGENT": "Agents",
"REPORTS_LABEL": "Etiquetes",
"REPORTS_INBOX": "Inbox",
"REPORTS_TEAM": "Team"
}, },
"CREATE_ACCOUNT": { "CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.", "NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",

View File

@@ -54,6 +54,7 @@
"ERROR": "Time on page is required" "ERROR": "Time on page is required"
}, },
"ENABLED": "Enable campaign", "ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "Add Campaign" "SUBMIT": "Add Campaign"
}, },
"API": { "API": {

View File

@@ -10,6 +10,7 @@
"SEARCH": { "SEARCH": {
"INPUT": "Hledat lidi, chaty, Uložené odpovědi .." "INPUT": "Hledat lidi, chaty, Uložené odpovědi .."
}, },
"FILTER_ALL": "Vše",
"STATUS_TABS": [ "STATUS_TABS": [
{ {
"NAME": "Otevřít", "NAME": "Otevřít",
@@ -48,11 +49,11 @@
}, },
{ {
"TEXT": "Čekající", "TEXT": "Čekající",
"VALUE": "pending" "VALUE": "čekající"
}, },
{ {
"TEXT": "Odložené", "TEXT": "Odložené",
"VALUE": "snoozed" "VALUE": "odložené"
} }
], ],
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -85,6 +86,8 @@
"VIEW_TWEET_IN_TWITTER": "Zobrazit tweet na Twitteru", "VIEW_TWEET_IN_TWITTER": "Zobrazit tweet na Twitteru",
"REPLY_TO_TWEET": "Odpovědět na tento tweet", "REPLY_TO_TWEET": "Odpovědět na tento tweet",
"NO_MESSAGES": "Žádné zprávy", "NO_MESSAGES": "Žádné zprávy",
"NO_CONTENT": "Žádný obsah k dispozici" "NO_CONTENT": "Žádný obsah k dispozici",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
"SHOW_QUOTED_TEXT": "Show Quoted Text"
} }
} }

View File

@@ -32,6 +32,8 @@
"NO_RESULT": "No labels found" "NO_RESULT": "No labels found"
} }
}, },
"MERGE_CONTACT": "Merge contact",
"CONTACT_ACTIONS": "Contact actions",
"MUTE_CONTACT": "Ztlumit konverzaci", "MUTE_CONTACT": "Ztlumit konverzaci",
"UNMUTE_CONTACT": "Zrušit ztlumení konverzace", "UNMUTE_CONTACT": "Zrušit ztlumení konverzace",
"MUTED_SUCCESS": "Tato konverzace je ztlumena na 6 hodin", "MUTED_SUCCESS": "Tato konverzace je ztlumena na 6 hodin",
@@ -54,6 +56,35 @@
"TITLE": "Vytvořit nový kontakt", "TITLE": "Vytvořit nový kontakt",
"DESC": "Přidat základní informace o kontaktu." "DESC": "Přidat základní informace o kontaktu."
}, },
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import",
"TITLE": "Import Contacts",
"DESC": "Import contacts through a CSV file.",
"DOWNLOAD_LABEL": "Download a sample csv.",
"FORM": {
"LABEL": "CSV File",
"SUBMIT": "Import",
"CANCEL": "Zrušit"
},
"SUCCESS_MESSAGE": "Contacts saved successfully",
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
},
"DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact",
"TITLE": "Delete contact",
"DESC": "Delete contact details",
"CONFIRM": {
"TITLE": "Potvrdit odstranění",
"MESSAGE": "Opravdu chcete odstranit ",
"PLACE_HOLDER": "Please type {contactName} to confirm",
"YES": "Ano, odstranit ",
"NO": "Ne, zachovat "
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
}
},
"CONTACT_FORM": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "Odeslat", "SUBMIT": "Odeslat",
@@ -213,17 +244,19 @@
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
"TITLE": "Merge contacts", "TITLE": "Merge contacts",
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.", "DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": { "PRIMARY": {
"TITLE": "Primary contact" "TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
}, },
"CHILD": { "CHILD": {
"TITLE": "Contact to merge", "TITLE": "Contact to merge",
"PLACEHOLDER": "Choose a contact" "PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "Summary", "TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.", "DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>." "ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
}, },
"SEARCH": { "SEARCH": {
@@ -236,7 +269,7 @@
"ERROR": "Select a child contact to merge" "ERROR": "Select a child contact to merge"
}, },
"SUCCESS_MESSAGE": "Contact merged successfully", "SUCCESS_MESSAGE": "Contact merged successfully",
"ERROR_MESSAGE": "Could not merge contcts, try again!" "ERROR_MESSAGE": "Could not merge contacts, try again!"
} }
} }
} }

View File

@@ -39,7 +39,10 @@
"OPEN_ACTION": "Otevřít", "OPEN_ACTION": "Otevřít",
"OPEN": "Více", "OPEN": "Více",
"CLOSE": "Zavřít", "CLOSE": "Zavřít",
"DETAILS": "Podrobnosti" "DETAILS": "Podrobnosti",
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
}, },
"RESOLVE_DROPDOWN": { "RESOLVE_DROPDOWN": {
"MARK_PENDING": "Mark as pending", "MARK_PENDING": "Mark as pending",
@@ -84,6 +87,7 @@
"CHANGE_AGENT": "Konverzace pověřená osoba změněna", "CHANGE_AGENT": "Konverzace pověřená osoba změněna",
"CHANGE_TEAM": "Tým konverzace se změnil", "CHANGE_TEAM": "Tým konverzace se změnil",
"FILE_SIZE_LIMIT": "Soubor překračuje limit {MAXIMUM_FILE_UPLOAD_SIZE} přílohy", "FILE_SIZE_LIMIT": "Soubor překračuje limit {MAXIMUM_FILE_UPLOAD_SIZE} přílohy",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Odeslal:", "SENT_BY": "Odeslal:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Vybrat agenta", "SELECT_AGENT": "Vybrat agenta",

View File

@@ -71,5 +71,13 @@
"assigned_conversation_new_message": "Nová zpráva", "assigned_conversation_new_message": "Nová zpráva",
"conversation_mention": "Zmínka" "conversation_mention": "Zmínka"
} }
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "Disconnected from Chatwoot"
},
"BUTTON": {
"REFRESH": "Refresh"
}
} }
} }

View File

@@ -56,6 +56,11 @@
"CHANNEL_AVATAR": { "CHANNEL_AVATAR": {
"LABEL": "Avatar kanálu" "LABEL": "Avatar kanálu"
}, },
"CHANNEL_WEBHOOK_URL": {
"LABEL": "URL webového háčku",
"PLACEHOLDER": "Enter your Webhook URL",
"ERROR": "Zadejte prosím platnou URL"
},
"CHANNEL_DOMAIN": { "CHANNEL_DOMAIN": {
"LABEL": "Doména webových stránek", "LABEL": "Doména webových stránek",
"PLACEHOLDER": "Zadejte doménu webu (např. acme.com)" "PLACEHOLDER": "Zadejte doménu webu (např. acme.com)"
@@ -92,8 +97,8 @@
"SUBMIT_BUTTON": "Vytvořit doručenou poštu" "SUBMIT_BUTTON": "Vytvořit doručenou poštu"
}, },
"TWILIO": { "TWILIO": {
"TITLE": "Twilio SMS/Whatsapp Channel", "TITLE": "Twilio SMS/WhatsApp Channel",
"DESC": "Integrate Twilio and start supporting your customers via SMS or Whatsapp.", "DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
"ACCOUNT_SID": { "ACCOUNT_SID": {
"LABEL": "SID účtu", "LABEL": "SID účtu",
"PLACEHOLDER": "Zadejte SID vašeho Twilio účtu", "PLACEHOLDER": "Zadejte SID vašeho Twilio účtu",
@@ -109,8 +114,8 @@
"ERROR": "Toto pole je povinné" "ERROR": "Toto pole je povinné"
}, },
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Název kanálu", "LABEL": "Název schránky",
"PLACEHOLDER": "Zadejte název kanálu", "PLACEHOLDER": "Please enter a inbox name",
"ERROR": "Toto pole je povinné" "ERROR": "Toto pole je povinné"
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
@@ -132,8 +137,34 @@
"DESC": "Start supporting your customers via SMS with Twilio integration." "DESC": "Start supporting your customers via SMS with Twilio integration."
}, },
"WHATSAPP": { "WHATSAPP": {
"TITLE": "Whatsapp Channel via Twilio", "TITLE": "WhatsApp Channel",
"DESC": "Start supporting your customers via Whatsapp with Twilio integration." "DESC": "Start supporting your customers via WhatsApp.",
"PROVIDERS": {
"LABEL": "API Provider",
"TWILIO": "Twilio",
"360_DIALOG": "360Dialog"
},
"INBOX_NAME": {
"LABEL": "Název schránky",
"PLACEHOLDER": "Please enter an inbox name",
"ERROR": "Toto pole je povinné"
},
"PHONE_NUMBER": {
"LABEL": "Telefonní číslo",
"PLACEHOLDER": "Zadejte prosím telefonní číslo, ze kterého bude zpráva odeslána.",
"ERROR": "Zadejte platnou hodnotu. Telefonní číslo by mělo začínat znakem `+`."
},
"API_KEY": {
"LABEL": "API key",
"SUBTITLE": "Configure the WhatsApp API key.",
"PLACEHOLDER": "API key",
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
"ERROR": "Please enter a valid value."
},
"SUBMIT_BUTTON": "Create WhatsApp Channel",
"API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
}
}, },
"API_CHANNEL": { "API_CHANNEL": {
"TITLE": "API Channel", "TITLE": "API Channel",
@@ -195,6 +226,10 @@
"SUBMIT_BUTTON": "Create LINE Channel", "SUBMIT_BUTTON": "Create LINE Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the LINE channel" "ERROR_MESSAGE": "We were not able to save the LINE channel"
},
"API_CALLBACK": {
"TITLE": "Callback URL",
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
} }
}, },
"TELEGRAM_CHANNEL": { "TELEGRAM_CHANNEL": {
@@ -212,7 +247,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Choose a channel", "TITLE": "Choose a channel",
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed." "DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Agenti", "TITLE": "Agenti",
@@ -266,6 +301,9 @@
"ENABLE_CSAT": { "ENABLE_CSAT": {
"ENABLED": "Povoleno", "ENABLED": "Povoleno",
"DISABLED": "Zakázáno" "DISABLED": "Zakázáno"
},
"ENABLE_HMAC": {
"LABEL": "Enable"
} }
}, },
"DELETE": { "DELETE": {
@@ -315,6 +353,8 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.", "AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
"HMAC_VERIFICATION": "User Identity Validation", "HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.", "HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
"INBOX_IDENTIFIER": "Inbox Identifier", "INBOX_IDENTIFIER": "Inbox Identifier",
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.", "INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
"FORWARD_EMAIL_TITLE": "Forward to Email", "FORWARD_EMAIL_TITLE": "Forward to Email",
@@ -350,7 +390,7 @@
"TIMEZONE_LABEL": "Vyberte časové pásmo", "TIMEZONE_LABEL": "Vyberte časové pásmo",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors", "UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.", "UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.", "TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
"DAY": { "DAY": {

View File

@@ -61,6 +61,258 @@
"PLACEHOLDER": "Select date range" "PLACEHOLDER": "Select date range"
} }
}, },
"AGENT_REPORTS": {
"HEADER": "Agents Overview",
"LOADING_CHART": "Načítání dat mapy...",
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
"DOWNLOAD_AGENT_REPORTS": "Stáhnout reporty agentů",
"FILTER_DROPDOWN_LABEL": "Vybrat agenta",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Konverzace",
"DESC": "( celkem)"
},
"INCOMING_MESSAGES": {
"NAME": "Příchozí zprávy",
"DESC": "( celkem)"
},
"OUTGOING_MESSAGES": {
"NAME": "Odchozí zprávy",
"DESC": "( celkem)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Čas první odpovědi",
"DESC": "(Průměrný)"
},
"RESOLUTION_TIME": {
"NAME": "Čas rozlišení",
"DESC": "(Průměrný)"
},
"RESOLUTION_COUNT": {
"NAME": "Počet rozlišení",
"DESC": "( celkem)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Posledních 7 dní"
},
{
"id": 1,
"name": "Posledních 30 dní"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"LABEL_REPORTS": {
"HEADER": "Labels Overview",
"LOADING_CHART": "Načítání dat mapy...",
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
"FILTER_DROPDOWN_LABEL": "Select Label",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Konverzace",
"DESC": "( celkem)"
},
"INCOMING_MESSAGES": {
"NAME": "Příchozí zprávy",
"DESC": "( celkem)"
},
"OUTGOING_MESSAGES": {
"NAME": "Odchozí zprávy",
"DESC": "( celkem)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Čas první odpovědi",
"DESC": "(Průměrný)"
},
"RESOLUTION_TIME": {
"NAME": "Čas rozlišení",
"DESC": "(Průměrný)"
},
"RESOLUTION_COUNT": {
"NAME": "Počet rozlišení",
"DESC": "( celkem)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Posledních 7 dní"
},
{
"id": 1,
"name": "Posledních 30 dní"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"INBOX_REPORTS": {
"HEADER": "Inbox Overview",
"LOADING_CHART": "Načítání dat mapy...",
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
"FILTER_DROPDOWN_LABEL": "Select Inbox",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Konverzace",
"DESC": "( celkem)"
},
"INCOMING_MESSAGES": {
"NAME": "Příchozí zprávy",
"DESC": "( celkem)"
},
"OUTGOING_MESSAGES": {
"NAME": "Odchozí zprávy",
"DESC": "( celkem)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Čas první odpovědi",
"DESC": "(Průměrný)"
},
"RESOLUTION_TIME": {
"NAME": "Čas rozlišení",
"DESC": "(Průměrný)"
},
"RESOLUTION_COUNT": {
"NAME": "Počet rozlišení",
"DESC": "( celkem)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Posledních 7 dní"
},
{
"id": 1,
"name": "Posledních 30 dní"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"LOADING_CHART": "Načítání dat mapy...",
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Konverzace",
"DESC": "( celkem)"
},
"INCOMING_MESSAGES": {
"NAME": "Příchozí zprávy",
"DESC": "( celkem)"
},
"OUTGOING_MESSAGES": {
"NAME": "Odchozí zprávy",
"DESC": "( celkem)"
},
"FIRST_RESPONSE_TIME": {
"NAME": "Čas první odpovědi",
"DESC": "(Průměrný)"
},
"RESOLUTION_TIME": {
"NAME": "Čas rozlišení",
"DESC": "(Průměrný)"
},
"RESOLUTION_COUNT": {
"NAME": "Počet rozlišení",
"DESC": "( celkem)"
}
},
"DATE_RANGE": [
{
"id": 0,
"name": "Posledních 7 dní"
},
{
"id": 1,
"name": "Posledních 30 dní"
},
{
"id": 2,
"name": "Last 3 months"
},
{
"id": 3,
"name": "Last 6 months"
},
{
"id": 4,
"name": "Last year"
},
{
"id": 5,
"name": "Custom date range"
}
],
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"CSAT_REPORTS": { "CSAT_REPORTS": {
"HEADER": "CSAT Reports", "HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.", "NO_RECORDS": "There are no CSAT survey responses available.",
@@ -87,4 +339,4 @@
} }
} }
} }
} }

View File

@@ -150,7 +150,11 @@
"CSAT": "CSAT", "CSAT": "CSAT",
"CAMPAIGNS": "Kampaně", "CAMPAIGNS": "Kampaně",
"ONGOING": "Ongoing", "ONGOING": "Ongoing",
"ONE_OFF": "One off" "ONE_OFF": "One off",
"REPORTS_AGENT": "Agenti",
"REPORTS_LABEL": "Štítky",
"REPORTS_INBOX": "Inbox",
"REPORTS_TEAM": "Team"
}, },
"CREATE_ACCOUNT": { "CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.", "NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",

View File

@@ -54,6 +54,7 @@
"ERROR": "Time on page is required" "ERROR": "Time on page is required"
}, },
"ENABLED": "Enable campaign", "ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "Add Campaign" "SUBMIT": "Add Campaign"
}, },
"API": { "API": {

View File

@@ -10,6 +10,7 @@
"SEARCH": { "SEARCH": {
"INPUT": "Søg efter Mennesker, Chats, Gemte svar .." "INPUT": "Søg efter Mennesker, Chats, Gemte svar .."
}, },
"FILTER_ALL": "Alle",
"STATUS_TABS": [ "STATUS_TABS": [
{ {
"NAME": "Åbn", "NAME": "Åbn",
@@ -85,6 +86,8 @@
"VIEW_TWEET_IN_TWITTER": "Se tweet på Twitter", "VIEW_TWEET_IN_TWITTER": "Se tweet på Twitter",
"REPLY_TO_TWEET": "Svar på dette tweet", "REPLY_TO_TWEET": "Svar på dette tweet",
"NO_MESSAGES": "No Messages", "NO_MESSAGES": "No Messages",
"NO_CONTENT": "No content available" "NO_CONTENT": "No content available",
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
"SHOW_QUOTED_TEXT": "Show Quoted Text"
} }
} }

View File

@@ -32,6 +32,8 @@
"NO_RESULT": "No labels found" "NO_RESULT": "No labels found"
} }
}, },
"MERGE_CONTACT": "Merge contact",
"CONTACT_ACTIONS": "Contact actions",
"MUTE_CONTACT": "Gør Samtale Lydløs", "MUTE_CONTACT": "Gør Samtale Lydløs",
"UNMUTE_CONTACT": "Fjern Lydløs", "UNMUTE_CONTACT": "Fjern Lydløs",
"MUTED_SUCCESS": "Denne samtale er gjort tavs i 6 timer", "MUTED_SUCCESS": "Denne samtale er gjort tavs i 6 timer",
@@ -54,6 +56,35 @@
"TITLE": "Create new contact", "TITLE": "Create new contact",
"DESC": "Add basic information details about the contact." "DESC": "Add basic information details about the contact."
}, },
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import",
"TITLE": "Import Contacts",
"DESC": "Import contacts through a CSV file.",
"DOWNLOAD_LABEL": "Download a sample csv.",
"FORM": {
"LABEL": "CSV File",
"SUBMIT": "Import",
"CANCEL": "Annuller"
},
"SUCCESS_MESSAGE": "Contacts saved successfully",
"ERROR_MESSAGE": "Der opstod en fejl. Prøv venligst igen"
},
"DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact",
"TITLE": "Delete contact",
"DESC": "Delete contact details",
"CONFIRM": {
"TITLE": "Bekræft Sletning",
"MESSAGE": "Er du sikker på du vil slette ",
"PLACE_HOLDER": "Please type {contactName} to confirm",
"YES": "Ja, Slet ",
"NO": "Nej, Behold "
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
}
},
"CONTACT_FORM": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "Send", "SUBMIT": "Send",
@@ -213,17 +244,19 @@
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
"TITLE": "Merge contacts", "TITLE": "Merge contacts",
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.", "DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": { "PRIMARY": {
"TITLE": "Primary contact" "TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
}, },
"CHILD": { "CHILD": {
"TITLE": "Contact to merge", "TITLE": "Contact to merge",
"PLACEHOLDER": "Choose a contact" "PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "Summary", "TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.", "DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>." "ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
}, },
"SEARCH": { "SEARCH": {
@@ -236,7 +269,7 @@
"ERROR": "Select a child contact to merge" "ERROR": "Select a child contact to merge"
}, },
"SUCCESS_MESSAGE": "Contact merged successfully", "SUCCESS_MESSAGE": "Contact merged successfully",
"ERROR_MESSAGE": "Could not merge contcts, try again!" "ERROR_MESSAGE": "Could not merge contacts, try again!"
} }
} }
} }

View File

@@ -39,7 +39,10 @@
"OPEN_ACTION": "Åbn", "OPEN_ACTION": "Åbn",
"OPEN": "Mere", "OPEN": "Mere",
"CLOSE": "Luk", "CLOSE": "Luk",
"DETAILS": "detaljer" "DETAILS": "detaljer",
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
}, },
"RESOLVE_DROPDOWN": { "RESOLVE_DROPDOWN": {
"MARK_PENDING": "Mark as pending", "MARK_PENDING": "Mark as pending",
@@ -84,6 +87,7 @@
"CHANGE_AGENT": "Samtaleansvarlig ændret", "CHANGE_AGENT": "Samtaleansvarlig ændret",
"CHANGE_TEAM": "Conversation team changed", "CHANGE_TEAM": "Conversation team changed",
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Sent by:", "SENT_BY": "Sent by:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Select Agent", "SELECT_AGENT": "Select Agent",

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