Compare commits
209
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40ad399f6 | ||
|
|
29478880ff | ||
|
|
c9f8cdb51b | ||
|
|
a2ebf92932 | ||
|
|
00eb5b152a | ||
|
|
0b2f539ad0 | ||
|
|
1ab3966b75 | ||
|
|
552d967c81 | ||
|
|
e5c8a68a76 | ||
|
|
15f50635e7 | ||
|
|
cf0d6dd7c6 | ||
|
|
fd1813949a | ||
|
|
6c480098f7 | ||
|
|
ae649a5b3f | ||
|
|
34675da6fa | ||
|
|
075b0292b5 | ||
|
|
0e9825f298 | ||
|
|
bf883794a4 | ||
|
|
3adaa2d602 | ||
|
|
60a312ace5 | ||
|
|
8c9a351c84 | ||
|
|
d28f512de4 | ||
|
|
376de685fb | ||
|
|
890515edfd | ||
|
|
79412ba2c6 | ||
|
|
03ebb6947b | ||
|
|
c53591f049 | ||
|
|
db9a32a4c0 | ||
|
|
2ae9bbb24e | ||
|
|
8dfe193461 | ||
|
|
27239ae14a | ||
|
|
80ff5e2d0a | ||
|
|
38240a68aa | ||
|
|
89710b406e | ||
|
|
f002870c6a | ||
|
|
b1a68759cf | ||
|
|
bf49a17efa | ||
|
|
6b0d1d77d3 | ||
|
|
db33b7d1dc | ||
|
|
76711d95ff | ||
|
|
17faa6c3b2 | ||
|
|
c1ac354c9b | ||
|
|
449503bb94 | ||
|
|
aad18e1ca4 | ||
|
|
fdc1123b18 | ||
|
|
0974b1e705 | ||
|
|
bc6e3e598f | ||
|
|
683f9d9a28 | ||
|
|
f9bf0883c6 | ||
|
|
ad8ba299c9 | ||
|
|
3ae64822e1 | ||
|
|
a49aaee9cf | ||
|
|
2d1f70eb79 | ||
|
|
4fc5f765de | ||
|
|
7efc2726d9 | ||
|
|
29a778697c | ||
|
|
90649e72bb | ||
|
|
1904ec7df4 | ||
|
|
96add30331 | ||
|
|
9c7148e2ad | ||
|
|
31c709be5c | ||
|
|
e750ee6d28 | ||
|
|
5b3f9ac1cd | ||
|
|
6c8dacfa0d | ||
|
|
feead30b0b | ||
|
|
dcd18072b4 | ||
|
|
4244945e7b | ||
|
|
17725e4dd0 | ||
|
|
d55bb63aa0 | ||
|
|
bda2d1d9a4 | ||
|
|
6e22695b85 | ||
|
|
146e46d79f | ||
|
|
0af27a2387 | ||
|
|
7380f0e7ce | ||
|
|
2c5a0abcce | ||
|
|
f23e33c1a9 | ||
|
|
5588d6e344 | ||
|
|
59ace66c6f | ||
|
|
decef1ad52 | ||
|
|
0e19a4196b | ||
|
|
04dd65687b | ||
|
|
5f503b1a57 | ||
|
|
5f6e974531 | ||
|
|
a023eabf59 | ||
|
|
cb1e25a487 | ||
|
|
bcf7c6c45a | ||
|
|
b411b6ca21 | ||
|
|
cd217a3f86 | ||
|
|
6c79642450 | ||
|
|
201d476720 | ||
|
|
c1a2e67c9e | ||
|
|
1948bef9ac | ||
|
|
5486f39f7e | ||
|
|
f018b0013c | ||
|
|
950f085e80 | ||
|
|
268e26625b | ||
|
|
7041d86e4d | ||
|
|
78398ee800 | ||
|
|
7b09b02737 | ||
|
|
af7631d9f1 | ||
|
|
e0f33e62af | ||
|
|
5d224f2e04 | ||
|
|
4cb981d81b | ||
|
|
f891cfbf47 | ||
|
|
e54c8a857d | ||
|
|
66229b0d6b | ||
|
|
3eb1d49577 | ||
|
|
677888bcde | ||
|
|
abbb4180ea | ||
|
|
d1cbc56d67 | ||
|
|
b579ff832e | ||
|
|
39d0748a5b | ||
|
|
b4d20689b7 | ||
|
|
3b84b0fc47 | ||
|
|
23ea829510 | ||
|
|
ebe9daea00 | ||
|
|
36b6c0cb9c | ||
|
|
16c36a78f0 | ||
|
|
4999f7ed23 | ||
|
|
e8f4ac632f | ||
|
|
3c7854ccdf | ||
|
|
1e70223ed7 | ||
|
|
f34cd3ea61 | ||
|
|
68b8d721a7 | ||
|
|
d876419b27 | ||
|
|
8455186e9f | ||
|
|
b6584ec68b | ||
|
|
29f18c7f18 | ||
|
|
11b27f9805 | ||
|
|
653e0335c0 | ||
|
|
013dab7da6 | ||
|
|
2ba81830f3 | ||
|
|
dfba4770bd | ||
|
|
63e702ca12 | ||
|
|
394fece846 | ||
|
|
24fbab94c3 | ||
|
|
61e03fa33a | ||
|
|
6c4b92f7f6 | ||
|
|
f023325b0e | ||
|
|
dd05482cb0 | ||
|
|
d94108bf3f | ||
|
|
ab872beb1d | ||
|
|
d02cfff4cc | ||
|
|
736f269c49 | ||
|
|
b067acfd4a | ||
|
|
232fa6fd18 | ||
|
|
483d55df91 | ||
|
|
3e54d3654b | ||
|
|
b6831d464e | ||
|
|
8c67ea56e7 | ||
|
|
54bc4c23df | ||
|
|
58f47eb02c | ||
|
|
4fc62ed9ae | ||
|
|
4a89bab23a | ||
|
|
7438f3b157 | ||
|
|
9551dbc25f | ||
|
|
2e54f4a67c | ||
|
|
6d9b2a6b33 | ||
|
|
35a9acf099 | ||
|
|
b9694a0818 | ||
|
|
7416bbb25e | ||
|
|
b94c89ebf1 | ||
|
|
78ce8a4652 | ||
|
|
04c874fe35 | ||
|
|
eabe548520 | ||
|
|
83231b9678 | ||
|
|
f96b54e57f | ||
|
|
d3bf828524 | ||
|
|
c20415f3ba | ||
|
|
6b2ed4fe79 | ||
|
|
eca9ca90f1 | ||
|
|
9018ec9678 | ||
|
|
e55fe33f1b | ||
|
|
3b0788c3be | ||
|
|
626e67b859 | ||
|
|
28db18ebbd | ||
|
|
b28721e10b | ||
|
|
f77db4d814 | ||
|
|
62d8ec7edb | ||
|
|
7b09fa4a03 | ||
|
|
980013abae | ||
|
|
5c729ccea5 | ||
|
|
ae52ca87aa | ||
|
|
415bb23c37 | ||
|
|
9e173fca3c | ||
|
|
bd918ee506 | ||
|
|
7c9884b853 | ||
|
|
7ffa669c5c | ||
|
|
0bc20873f6 | ||
|
|
88de3359a5 | ||
|
|
a3d008da06 | ||
|
|
cbae95422d | ||
|
|
081c845c56 | ||
|
|
1e54d6deb3 | ||
|
|
bb5c6a416a | ||
|
|
6a07251007 | ||
|
|
759a66dd21 | ||
|
|
8189dd932c | ||
|
|
b6ba0f343e | ||
|
|
86ca90aa15 | ||
|
|
26e8877cd9 | ||
|
|
1b63adfb2e | ||
|
|
e27274a5a8 | ||
|
|
e5c198f839 | ||
|
|
72ba429159 | ||
|
|
3227cf7ff3 | ||
|
|
f0740120a8 | ||
|
|
3ea54065b1 | ||
|
|
6f19546c3c |
+20
-1
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
|||||||
# Mail Incoming
|
# Mail Incoming
|
||||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||||
# Set this to appropriate ingress channel with regards to incoming emails
|
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||||
# Possible values are :
|
# Possible values are :
|
||||||
# relay for Exim, Postfix, Qmail
|
# relay for Exim, Postfix, Qmail
|
||||||
# mailgun for Mailgun
|
# mailgun for Mailgun
|
||||||
@@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
|
|||||||
RAILS_INBOUND_EMAIL_SERVICE=
|
RAILS_INBOUND_EMAIL_SERVICE=
|
||||||
# Use one of the following based on the email ingress service
|
# Use one of the following based on the email ingress service
|
||||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||||
|
# Set this to a password of your choice and use it in the Inbound webhook
|
||||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||||
|
|
||||||
MAILGUN_INGRESS_SIGNING_KEY=
|
MAILGUN_INGRESS_SIGNING_KEY=
|
||||||
MANDRILL_INGRESS_API_KEY=
|
MANDRILL_INGRESS_API_KEY=
|
||||||
|
|
||||||
|
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||||
|
# Inbound webhook URL format:
|
||||||
|
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||||
|
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||||
|
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||||
|
# For Postmark
|
||||||
|
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
ACTIVE_STORAGE_SERVICE=local
|
ACTIVE_STORAGE_SERVICE=local
|
||||||
|
|
||||||
@@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
|||||||
# MICROSOFT CLARITY
|
# MICROSOFT CLARITY
|
||||||
# MS_CLARITY_TOKEN=xxxxxxxxx
|
# MS_CLARITY_TOKEN=xxxxxxxxx
|
||||||
|
|
||||||
|
# GOOGLE_TAG_MANAGER
|
||||||
|
# GOOGLE_TAG = GTM-XXXXXXX
|
||||||
|
|
||||||
## Scout
|
## Scout
|
||||||
## https://scoutapm.com/docs/ruby/configuration
|
## https://scoutapm.com/docs/ruby/configuration
|
||||||
# SCOUT_KEY=YOURKEY
|
# SCOUT_KEY=YOURKEY
|
||||||
@@ -240,3 +253,9 @@ AZURE_APP_SECRET=
|
|||||||
|
|
||||||
# Sentiment analysis model file path
|
# Sentiment analysis model file path
|
||||||
SENTIMENT_FILE_PATH=
|
SENTIMENT_FILE_PATH=
|
||||||
|
|
||||||
|
|
||||||
|
# Housekeeping/Performance related configurations
|
||||||
|
# Set to true if you want to remove stale contact inboxes
|
||||||
|
# contact_inboxes with no conversation older than 90 days will be removed
|
||||||
|
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
'prettier',
|
'prettier',
|
||||||
'plugin:vue/recommended',
|
'plugin:vue/recommended',
|
||||||
'plugin:storybook/recommended',
|
'plugin:storybook/recommended',
|
||||||
|
'plugin:cypress/recommended',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@babel/eslint-parser',
|
parser: '@babel/eslint-parser',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ gem 'browser'
|
|||||||
gem 'hashie'
|
gem 'hashie'
|
||||||
gem 'jbuilder'
|
gem 'jbuilder'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
gem 'responders'
|
gem 'responders', '>= 3.1.1'
|
||||||
gem 'rest-client'
|
gem 'rest-client'
|
||||||
gem 'telephone_number'
|
gem 'telephone_number'
|
||||||
gem 'time_diff'
|
gem 'time_diff'
|
||||||
@@ -67,7 +67,7 @@ gem 'webpacker'
|
|||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
##--- gems for authentication & authorization ---##
|
##--- gems for authentication & authorization ---##
|
||||||
gem 'devise'
|
gem 'devise', '>= 4.9.3'
|
||||||
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||||
gem 'devise_token_auth'
|
gem 'devise_token_auth'
|
||||||
# authorization
|
# authorization
|
||||||
@@ -75,7 +75,7 @@ gem 'jwt'
|
|||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
# super admin
|
# super admin
|
||||||
gem 'administrate', '>= 0.19.0'
|
gem 'administrate', '>= 0.19.0'
|
||||||
gem 'administrate-field-active_storage'
|
gem 'administrate-field-active_storage', '>= 1.0.0'
|
||||||
gem 'administrate-field-belongs_to_search'
|
gem 'administrate-field-belongs_to_search'
|
||||||
|
|
||||||
##--- gems for pubsub service ---##
|
##--- gems for pubsub service ---##
|
||||||
@@ -92,7 +92,7 @@ gem 'twitty', '~> 0.1.5'
|
|||||||
# facebook client
|
# facebook client
|
||||||
gem 'koala'
|
gem 'koala'
|
||||||
# slack client
|
# slack client
|
||||||
gem 'slack-ruby-client', '~> 2.0.0'
|
gem 'slack-ruby-client', '~> 2.2.0'
|
||||||
# for dialogflow integrations
|
# for dialogflow integrations
|
||||||
gem 'google-cloud-dialogflow-v2'
|
gem 'google-cloud-dialogflow-v2'
|
||||||
gem 'grpc'
|
gem 'grpc'
|
||||||
@@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
|
|||||||
gem 'ddtrace', require: false
|
gem 'ddtrace', require: false
|
||||||
gem 'elastic-apm', require: false
|
gem 'elastic-apm', require: false
|
||||||
gem 'newrelic_rpm', require: false
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', require: false
|
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||||
gem 'scout_apm', require: false
|
gem 'scout_apm', require: false
|
||||||
gem 'sentry-rails', '>= 5.11.0', require: false
|
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||||
gem 'sentry-ruby', require: false
|
gem 'sentry-ruby', require: false
|
||||||
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
gem 'sentry-sidekiq', '>= 5.14.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '>= 7.1.3'
|
gem 'sidekiq', '>= 7.1.3'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron', '>= 1.10.1'
|
gem 'sidekiq-cron', '>= 1.12.0'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@@ -154,12 +154,12 @@ gem 'stripe'
|
|||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
|
||||||
# Include logrange conditionally in intializer using env variable
|
# Include logrange conditionally in intializer using env variable
|
||||||
gem 'lograge', '~> 0.13.0', require: false
|
gem 'lograge', '~> 0.14.0', require: false
|
||||||
|
|
||||||
# worked with microsoft refresh token
|
# worked with microsoft refresh token
|
||||||
gem 'omniauth-oauth2'
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
gem 'audited', '~> 5.4', '>= 5.4.0'
|
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||||
|
|
||||||
# need for google auth
|
# need for google auth
|
||||||
gem 'omniauth'
|
gem 'omniauth'
|
||||||
@@ -198,8 +198,10 @@ group :development do
|
|||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
|
||||||
# profiling
|
# profiling
|
||||||
gem 'rack-mini-profiler', '>= 3.1.1', require: false
|
gem 'rack-mini-profiler', '>= 3.2.0', require: false
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
|
# Should install the associated chrome extension to view query logs
|
||||||
|
gem 'meta_request'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
@@ -222,7 +224,7 @@ group :development, :test do
|
|||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
gem 'climate_control'
|
gem 'climate_control'
|
||||||
gem 'debug', '~> 1.8'
|
gem 'debug', '~> 1.8'
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails', '>= 6.4.2'
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
|
|||||||
+66
-59
@@ -113,7 +113,7 @@ GEM
|
|||||||
kaminari (>= 1.0)
|
kaminari (>= 1.0)
|
||||||
sassc-rails (~> 2.1)
|
sassc-rails (~> 2.1)
|
||||||
selectize-rails (~> 0.6)
|
selectize-rails (~> 0.6)
|
||||||
administrate-field-active_storage (0.4.2)
|
administrate-field-active_storage (1.0.0)
|
||||||
administrate (>= 0.2.2)
|
administrate (>= 0.2.2)
|
||||||
rails (>= 7.0)
|
rails (>= 7.0)
|
||||||
administrate-field-belongs_to_search (0.8.0)
|
administrate-field-belongs_to_search (0.8.0)
|
||||||
@@ -126,9 +126,9 @@ GEM
|
|||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
attr_extras (7.1.0)
|
attr_extras (7.1.0)
|
||||||
audited (5.4.0)
|
audited (5.4.1)
|
||||||
activerecord (>= 5.0, < 7.2)
|
activerecord (>= 5.0, < 7.7)
|
||||||
request_store (~> 1.2)
|
activesupport (>= 5.0, < 7.7)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.760.0)
|
aws-partitions (1.760.0)
|
||||||
aws-sdk-core (3.171.1)
|
aws-sdk-core (3.171.1)
|
||||||
@@ -148,7 +148,8 @@ GEM
|
|||||||
barnes (0.0.9)
|
barnes (0.0.9)
|
||||||
multi_json (~> 1)
|
multi_json (~> 1)
|
||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
bcrypt (3.1.18)
|
base64 (0.1.1)
|
||||||
|
bcrypt (3.1.19)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
blingfire (0.1.8)
|
blingfire (0.1.8)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
@@ -174,7 +175,7 @@ GEM
|
|||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
csv-safe (3.2.1)
|
csv-safe (3.2.1)
|
||||||
cypress-on-rails (1.13.1)
|
cypress-on-rails (1.16.0)
|
||||||
rack
|
rack
|
||||||
database_cleaner (2.0.2)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (>= 2, < 3)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
@@ -182,7 +183,7 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
ddtrace (1.11.1)
|
ddtrace (1.11.1)
|
||||||
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
|
||||||
libdatadog (~> 2.0.0.1.0)
|
libdatadog (~> 2.0.0.1.0)
|
||||||
@@ -193,7 +194,7 @@ GEM
|
|||||||
irb (>= 1.5.0)
|
irb (>= 1.5.0)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.1)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.2)
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -229,10 +230,10 @@ GEM
|
|||||||
facebook-messenger (2.0.1)
|
facebook-messenger (2.0.1)
|
||||||
httparty (~> 0.13, >= 0.13.7)
|
httparty (~> 0.13, >= 0.13.7)
|
||||||
rack (>= 1.4.5)
|
rack (>= 1.4.5)
|
||||||
factory_bot (6.2.1)
|
factory_bot (6.4.2)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.2)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
@@ -255,13 +256,13 @@ GEM
|
|||||||
fcm (1.0.8)
|
fcm (1.0.8)
|
||||||
faraday (>= 1.0.0, < 3.0)
|
faraday (>= 1.0.0, < 3.0)
|
||||||
googleauth (~> 1)
|
googleauth (~> 1)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
flag_shih_tzu (0.3.23)
|
flag_shih_tzu (0.3.23)
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
fugit (1.8.1)
|
fugit (1.9.0)
|
||||||
et-orbi (~> 1, >= 1.2.7)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
gapic-common (0.18.0)
|
gapic-common (0.18.0)
|
||||||
@@ -273,7 +274,7 @@ GEM
|
|||||||
googleauth (~> 1.0)
|
googleauth (~> 1.0)
|
||||||
grpc (~> 1.36)
|
grpc (~> 1.36)
|
||||||
geocoder (1.8.1)
|
geocoder (1.8.1)
|
||||||
gli (2.21.0)
|
gli (2.21.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.2)
|
||||||
@@ -434,12 +435,12 @@ GEM
|
|||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -450,13 +451,16 @@ GEM
|
|||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
maxminddb (0.1.22)
|
maxminddb (0.1.22)
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
|
meta_request (0.7.4)
|
||||||
|
rack-contrib (>= 1.1, < 3)
|
||||||
|
railties (>= 3.0.0, < 7.1)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.5)
|
||||||
minitest (5.20.0)
|
minitest (5.20.0)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
@@ -468,29 +472,30 @@ GEM
|
|||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.5)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic-sidekiq-metrics (1.6.1)
|
newrelic-sidekiq-metrics (1.6.2)
|
||||||
newrelic_rpm (~> 8)
|
newrelic_rpm (>= 8.0.0)
|
||||||
sidekiq
|
sidekiq
|
||||||
newrelic_rpm (8.16.0)
|
newrelic_rpm (9.6.0)
|
||||||
nio4r (2.5.9)
|
base64
|
||||||
nokogiri (1.15.4)
|
nio4r (2.6.0)
|
||||||
|
nokogiri (1.15.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
nokogiri (1.15.5-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-x86_64-darwin)
|
nokogiri (1.15.5-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-x86_64-linux)
|
nokogiri (1.15.5-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
numo-narray (0.9.2.1)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
@@ -553,13 +558,15 @@ GEM
|
|||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.7.1)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
|
rack-contrib (2.4.0)
|
||||||
|
rack (< 4)
|
||||||
rack-cors (2.0.1)
|
rack-cors (2.0.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.1.1)
|
rack-mini-profiler (3.2.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-protection (3.0.6)
|
rack-protection (3.0.6)
|
||||||
rack
|
rack
|
||||||
@@ -597,13 +604,13 @@ GEM
|
|||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.1.0)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (5.0.6)
|
redis (5.0.6)
|
||||||
redis-client (>= 0.9.0)
|
redis-client (>= 0.9.0)
|
||||||
redis-client (0.17.0)
|
redis-client (0.19.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
@@ -616,7 +623,7 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rest-client (2.1.0)
|
rest-client (2.1.0)
|
||||||
@@ -702,23 +709,23 @@ GEM
|
|||||||
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 (5.11.0)
|
sentry-rails (5.14.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.11.0)
|
sentry-ruby (~> 5.14.0)
|
||||||
sentry-ruby (5.11.0)
|
sentry-ruby (5.14.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.11.0)
|
sentry-sidekiq (5.14.0)
|
||||||
sentry-ruby (~> 5.11.0)
|
sentry-ruby (~> 5.14.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (5.3.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.1.3)
|
sidekiq (7.2.0)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.14.0)
|
||||||
sidekiq-cron (1.10.1)
|
sidekiq-cron (1.12.0)
|
||||||
fugit (~> 1.8)
|
fugit (~> 1.8)
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
@@ -732,13 +739,12 @@ GEM
|
|||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
simplecov-html (0.10.2)
|
simplecov-html (0.10.2)
|
||||||
slack-ruby-client (2.0.0)
|
slack-ruby-client (2.2.0)
|
||||||
faraday (>= 2.0)
|
faraday (>= 2.0)
|
||||||
faraday-mashify
|
faraday-mashify
|
||||||
faraday-multipart
|
faraday-multipart
|
||||||
gli
|
gli
|
||||||
hashie
|
hashie
|
||||||
websocket-driver
|
|
||||||
snaky_hash (2.0.1)
|
snaky_hash (2.0.1)
|
||||||
hashie
|
hashie
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
version_gem (~> 1.1, >= 1.1.1)
|
||||||
@@ -746,7 +752,7 @@ GEM
|
|||||||
spring-watcher-listen (2.1.0)
|
spring-watcher-listen (2.1.0)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
spring (>= 4)
|
spring (>= 4)
|
||||||
sprockets (4.2.0)
|
sprockets (4.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (>= 2.2.4, < 4)
|
rack (>= 2.2.4, < 4)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.4.2)
|
||||||
@@ -759,12 +765,12 @@ GEM
|
|||||||
stripe (8.5.0)
|
stripe (8.5.0)
|
||||||
telephone_number (1.4.20)
|
telephone_number (1.4.20)
|
||||||
test-prof (1.2.1)
|
test-prof (1.2.1)
|
||||||
thor (1.2.2)
|
thor (1.3.0)
|
||||||
tilt (2.2.0)
|
tilt (2.3.0)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
i18n
|
i18n
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
twilio-ruby (5.77.0)
|
twilio-ruby (5.77.0)
|
||||||
faraday (>= 0.9, < 3.0)
|
faraday (>= 0.9, < 3.0)
|
||||||
@@ -817,7 +823,7 @@ GEM
|
|||||||
working_hours (1.4.1)
|
working_hours (1.4.1)
|
||||||
activesupport (>= 3.2)
|
activesupport (>= 3.2)
|
||||||
tzinfo
|
tzinfo
|
||||||
zeitwerk (2.6.11)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
@@ -835,11 +841,11 @@ DEPENDENCIES
|
|||||||
activerecord-import
|
activerecord-import
|
||||||
acts-as-taggable-on
|
acts-as-taggable-on
|
||||||
administrate (>= 0.19.0)
|
administrate (>= 0.19.0)
|
||||||
administrate-field-active_storage
|
administrate-field-active_storage (>= 1.0.0)
|
||||||
administrate-field-belongs_to_search
|
administrate-field-belongs_to_search
|
||||||
annotate
|
annotate
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.4, >= 5.4.0)
|
audited (~> 5.4, >= 5.4.1)
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage-blob!
|
azure-storage-blob!
|
||||||
barnes
|
barnes
|
||||||
@@ -856,7 +862,7 @@ DEPENDENCIES
|
|||||||
database_cleaner
|
database_cleaner
|
||||||
ddtrace
|
ddtrace
|
||||||
debug (~> 1.8)
|
debug (~> 1.8)
|
||||||
devise
|
devise (>= 4.9.3)
|
||||||
devise-secure_password!
|
devise-secure_password!
|
||||||
devise_token_auth
|
devise_token_auth
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
@@ -864,7 +870,7 @@ DEPENDENCIES
|
|||||||
elastic-apm
|
elastic-apm
|
||||||
email_reply_trimmer
|
email_reply_trimmer
|
||||||
facebook-messenger
|
facebook-messenger
|
||||||
factory_bot_rails
|
factory_bot_rails (>= 6.4.2)
|
||||||
faker
|
faker
|
||||||
fcm
|
fcm
|
||||||
flag_shih_tzu
|
flag_shih_tzu
|
||||||
@@ -892,11 +898,12 @@ DEPENDENCIES
|
|||||||
line-bot-api
|
line-bot-api
|
||||||
liquid
|
liquid
|
||||||
listen
|
listen
|
||||||
lograge (~> 0.13.0)
|
lograge (~> 0.14.0)
|
||||||
maxminddb
|
maxminddb
|
||||||
|
meta_request
|
||||||
mock_redis
|
mock_redis
|
||||||
neighbor
|
neighbor
|
||||||
newrelic-sidekiq-metrics
|
newrelic-sidekiq-metrics (>= 1.6.2)
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
omniauth
|
omniauth
|
||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
@@ -911,12 +918,12 @@ DEPENDENCIES
|
|||||||
pundit
|
pundit
|
||||||
rack-attack (>= 6.7.0)
|
rack-attack (>= 6.7.0)
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler (>= 3.1.1)
|
rack-mini-profiler (>= 3.2.0)
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 7.0.8.0)
|
rails (~> 7.0.8.0)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders (>= 3.1.1)
|
||||||
rest-client
|
rest-client
|
||||||
reverse_markdown
|
reverse_markdown
|
||||||
rspec-rails
|
rspec-rails
|
||||||
@@ -928,14 +935,14 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.11.0)
|
sentry-rails (>= 5.14.0)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.11.0)
|
sentry-sidekiq (>= 5.14.0)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (>= 7.1.3)
|
sidekiq (>= 7.1.3)
|
||||||
sidekiq-cron (>= 1.10.1)
|
sidekiq-cron (>= 1.12.0)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client (~> 2.0.0)
|
slack-ruby-client (~> 2.2.0)
|
||||||
spring
|
spring
|
||||||
spring-watcher-listen
|
spring-watcher-listen
|
||||||
squasher
|
squasher
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
backend: RAILS_ENV=test bin/rails s -p 5050
|
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||||
frontend: bin/webpack-dev-server
|
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||||
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<p align="center">
|
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
|
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||||
|
|
||||||
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
|
___
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
# Chatwoot
|
||||||
|
|
||||||
|
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||||
|
<p>
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||||
</a>
|
</a>
|
||||||
@@ -13,9 +15,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
___
|
<p>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||||
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
|
||||||
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
|
||||||
@@ -29,9 +29,8 @@ ___
|
|||||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
|
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||||
|
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||||
|
|
||||||
|
|
||||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.1.0
|
3.3.1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.4.0
|
2.7.0
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
@import 'components/flashes';
|
@import 'components/flashes';
|
||||||
@import 'components/form-actions';
|
@import 'components/form-actions';
|
||||||
@import 'components/main-content';
|
@import 'components/main-content';
|
||||||
@import 'components/navigation';
|
|
||||||
@import 'components/pagination';
|
@import 'components/pagination';
|
||||||
@import 'components/search';
|
@import 'components/search';
|
||||||
@import 'components/reports';
|
@import 'components/reports';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
html {
|
html {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 10px;
|
font-size: 16px;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
.attribute-data {
|
.attribute-data {
|
||||||
float: left;
|
float: left;
|
||||||
margin-bottom: $base-spacing;
|
margin-bottom: $base-spacing;
|
||||||
margin-left: 2rem;
|
margin-left: 1.25rem;
|
||||||
width: calc(84% - 1rem);
|
width: calc(84% - 0.625rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribute--nested {
|
.attribute--nested {
|
||||||
|
|||||||
@@ -9,22 +9,22 @@
|
|||||||
|
|
||||||
.field-unit__label {
|
.field-unit__label {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 1rem;
|
margin-left: 0.625rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: calc(15% - 1rem);
|
width: calc(15% - 0.625rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-unit__field {
|
.field-unit__field {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 2rem;
|
margin-left: 1.25rem;
|
||||||
max-width: 50rem;
|
max-width: 31.15rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-unit--nested {
|
.field-unit--nested {
|
||||||
border: $base-border;
|
border: $base-border;
|
||||||
margin-left: 7.5%;
|
margin-left: 7.5%;
|
||||||
max-width: 60rem;
|
max-width: 37.5rem;
|
||||||
padding: $small-spacing;
|
padding: $small-spacing;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.form-actions {
|
.form-actions {
|
||||||
margin-left: calc(15% + 2rem);
|
margin-left: calc(15% + 1.25rem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
table {
|
table {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-top: $space-two;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content__header {
|
.main-content__header {
|
||||||
@@ -20,7 +24,7 @@
|
|||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border-bottom: 1px solid $color-border;
|
border-bottom: 1px solid $color-border;
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 5.6rem;
|
min-height: 3.5rem;
|
||||||
padding: $space-small $space-normal;
|
padding: $space-small $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
.logo-brand {
|
|
||||||
margin-bottom: $space-normal;
|
|
||||||
padding: $space-normal $space-smaller $space-small;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-bottom: $space-smaller;
|
|
||||||
max-height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation {
|
|
||||||
background: $white;
|
|
||||||
border-right: 1px solid $color-border;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-size: $font-size-default;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: $space-normal;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
width: 21rem;
|
|
||||||
z-index: 1023;
|
|
||||||
|
|
||||||
li {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $color-gray;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
min-width: $space-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
margin: $space-slab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation__link {
|
|
||||||
background-color: transparent;
|
|
||||||
color: $color-gray;
|
|
||||||
display: block;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: $space-smaller;
|
|
||||||
padding: $space-small;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $blue;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&.navigation__link--active {
|
|
||||||
background-color: $color-background;
|
|
||||||
border-radius: $base-border-radius;
|
|
||||||
color: $blue;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout {
|
|
||||||
bottom: $space-normal;
|
|
||||||
left: $space-normal;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-version {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-small;
|
|
||||||
padding-top: $space-smaller;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.search {
|
.search {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 2rem;
|
margin-right: 1.25rem;
|
||||||
max-width: 44rem;
|
max-width: 27.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Typography
|
// Typography
|
||||||
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
sans-serif !default;
|
sans-serif !default;
|
||||||
$heading-font-family: $base-font-family !default;
|
$heading-font-family: $base-font-family !default;
|
||||||
|
|
||||||
$base-font-size: 14px !default;
|
$base-font-size: 16px !default;
|
||||||
|
|
||||||
$base-line-height: 1.5 !default;
|
$base-line-height: 1.5 !default;
|
||||||
$heading-line-height: 1.2 !default;
|
$heading-line-height: 1.2 !default;
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
// Font sizes
|
// Font sizes
|
||||||
$font-size-nano: 0.8rem;
|
$font-size-nano: 0.5rem;
|
||||||
$font-size-micro: 1.0rem;
|
$font-size-micro: 0.675rem;
|
||||||
$font-size-mini: 1.2rem;
|
$font-size-mini: 0.75rem;
|
||||||
$font-size-small: 1.4rem;
|
$font-size-small: 0.875rem;
|
||||||
$font-size-default: 1.6rem;
|
$font-size-default: 1rem;
|
||||||
$font-size-medium: 1.8rem;
|
$font-size-medium: 1.125rem;
|
||||||
$font-size-large: 2.2rem;
|
$font-size-large: 1.375rem;
|
||||||
$font-size-big: 2.4rem;
|
$font-size-big: 1.5rem;
|
||||||
$font-size-bigger: 3.0rem;
|
$font-size-bigger: 1.75rem;
|
||||||
$font-size-mega: 3.4rem;
|
$font-size-mega: 2.125rem;
|
||||||
$font-size-giga: 4.0rem;
|
$font-size-giga: 2.5rem;
|
||||||
|
|
||||||
// spaces
|
// spaces
|
||||||
$zero: 0;
|
$zero: 0;
|
||||||
$space-micro: 0.2rem;
|
$space-micro: 0.125rem;
|
||||||
$space-smaller: 0.4rem;
|
$space-smaller: 0.25rem;
|
||||||
$space-small: 0.8rem;
|
$space-small: 0.5rem;
|
||||||
$space-one: 1rem;
|
$space-one: 0.675rem;
|
||||||
$space-slab: 1.2rem;
|
$space-slab: 0.75rem;
|
||||||
$space-normal: 1.6rem;
|
$space-normal: 1rem;
|
||||||
$space-two: 2.0rem;
|
$space-two: 1.25rem;
|
||||||
$space-medium: 2.4rem;
|
$space-medium: 1.5rem;
|
||||||
$space-large: 3.2rem;
|
$space-large: 2rem;
|
||||||
$space-larger: 4.8rem;
|
$space-larger: 3rem;
|
||||||
$space-jumbo: 6.4rem;
|
$space-jumbo: 4rem;
|
||||||
$space-mega: 10.0rem;
|
$space-mega: 6.25rem;
|
||||||
|
|
||||||
// font-weight
|
// font-weight
|
||||||
$font-weight-feather: 100;
|
$font-weight-feather: 100;
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
build_contact_inbox
|
build_contact_inbox
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
@@ -93,6 +95,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
source_id: response.identifier,
|
source_id: response.identifier,
|
||||||
|
content_attributes: {
|
||||||
|
in_reply_to_external_id: response.in_reply_to_external_id
|
||||||
|
},
|
||||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -105,11 +110,15 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
def contact_params
|
def contact_params
|
||||||
begin
|
begin
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
result = k.get_object(@sender_id) || {}
|
result = k.get_object(@sender_id) || {}
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
@@ -127,4 +136,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
end
|
end
|
||||||
process_contact_params_result(result)
|
process_contact_params_result(result)
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize
|
||||||
|
# rubocop:enable Metrics/MethodLength
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError => e
|
||||||
|
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
|
||||||
|
Rails.logger.error e
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@@ -76,6 +78,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_reply_attributes
|
||||||
|
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
|
||||||
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||||
return if message_content.blank? && all_unsupported_files?
|
return if message_content.blank? && all_unsupported_files?
|
||||||
@@ -118,7 +124,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
message_type: message_type,
|
message_type: message_type,
|
||||||
source_id: message_identifier,
|
source_id: message_identifier,
|
||||||
content: message_content,
|
content: message_content,
|
||||||
sender: @outgoing_echo ? nil : contact
|
sender: @outgoing_echo ? nil : contact,
|
||||||
|
content_attributes: {
|
||||||
|
in_reply_to_external_id: message_reply_attributes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
|||||||
@user = user
|
@user = user
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@message_type = params[:message_type] || 'outgoing'
|
||||||
@attachments = params[:attachments]
|
@attachments = params[:attachments]
|
||||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||||
return unless params.instance_of?(ActionController::Parameters)
|
return unless params.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
@items = content_attributes&.dig(:items)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Extracts content attributes from the given params.
|
||||||
|
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||||
|
# - Attempts to parse a JSON string if content is a string.
|
||||||
|
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||||
|
def content_attributes
|
||||||
|
params = convert_to_hash(@params)
|
||||||
|
content_attributes = params.fetch(:content_attributes, {})
|
||||||
|
|
||||||
|
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||||
|
return content_attributes if content_attributes.is_a?(Hash)
|
||||||
|
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts the given object to a hash.
|
||||||
|
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||||
|
# Otherwise, returns the object as-is.
|
||||||
|
def convert_to_hash(obj)
|
||||||
|
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
|
obj
|
||||||
|
end
|
||||||
|
|
||||||
|
# Attempts to parse a string as JSON.
|
||||||
|
# If successful, returns the parsed hash with symbolized names.
|
||||||
|
# If unsuccessful, returns nil.
|
||||||
|
def parse_json(content)
|
||||||
|
JSON.parse(content, symbolize_names: true)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
def process_attachments
|
def process_attachments
|
||||||
return if @attachments.blank?
|
return if @attachments.blank?
|
||||||
|
|
||||||
@@ -117,7 +149,8 @@ class Messages::MessageBuilder
|
|||||||
content_type: @params[:content_type],
|
content_type: @params[:content_type],
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id],
|
||||||
|
source_id: @params[:source_id]
|
||||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class NotificationBuilder
|
class NotificationBuilder
|
||||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return unless user_subscribed_to_notification?
|
return unless user_subscribed_to_notification?
|
||||||
@@ -9,7 +9,7 @@ class NotificationBuilder
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def secondary_actor
|
def current_user
|
||||||
Current.user
|
Current.user
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ class NotificationBuilder
|
|||||||
notification_type: notification_type,
|
notification_type: notification_type,
|
||||||
account: account,
|
account: account,
|
||||||
primary_actor: primary_actor,
|
primary_actor: primary_actor,
|
||||||
secondary_actor: secondary_actor
|
# secondary_actor is secondary_actor if present, else current_user
|
||||||
|
secondary_actor: secondary_actor || current_user
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ class V2::ReportBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def timeseries
|
def timeseries
|
||||||
send(params[:metric])
|
return send(params[:metric]) if metric_valid?
|
||||||
|
|
||||||
|
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
||||||
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
@@ -53,6 +56,16 @@ class V2::ReportBuilder
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def metric_valid?
|
||||||
|
%w[conversations_count
|
||||||
|
incoming_messages_count
|
||||||
|
outgoing_messages_count
|
||||||
|
avg_first_response_time
|
||||||
|
avg_resolution_time reply_time
|
||||||
|
resolutions_count
|
||||||
|
reply_time].include?(params[:metric])
|
||||||
|
end
|
||||||
|
|
||||||
def inbox
|
def inbox
|
||||||
@inbox ||= account.inboxes.find(params[:id])
|
@inbox ||= account.inboxes.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
|
|||||||
mergee_contact: @mergee_contact
|
mergee_contact: @mergee_contact
|
||||||
)
|
)
|
||||||
contact_merge_action.perform
|
contact_merge_action.perform
|
||||||
render json: @base_contact
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@agent_bot = Current.account.agent_bots.create!(permitted_params)
|
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@agent_bot.update!(permitted_params)
|
@agent_bot.update!(permitted_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def avatar
|
||||||
|
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
|
||||||
|
@agent_bot
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -30,6 +37,10 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
|
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_avatar_from_url
|
||||||
|
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
@articles_count = @all_articles.count
|
@articles_count = @all_articles.count
|
||||||
|
|
||||||
@articles = if list_params[:category_slug].present?
|
@articles = if list_params[:category_slug].present?
|
||||||
@all_articles.order_by_position.page(@current_page).per(50)
|
@all_articles.order_by_position.page(@current_page)
|
||||||
else
|
else
|
||||||
@all_articles.order_by_updated_at.page(@current_page)
|
@all_articles.order_by_updated_at.page(@current_page)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
|||||||
|
|
||||||
def category_params
|
def category_params
|
||||||
params.require(:category).permit(
|
params.require(:category).permit(
|
||||||
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
|
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
|
||||||
@contact.save!
|
@contact.save!
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
process_avatar
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@contact.assign_attributes(contact_update_params)
|
@contact.assign_attributes(contact_update_params)
|
||||||
@contact.save!
|
@contact.save!
|
||||||
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@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 process_avatar
|
def process_avatar_from_url
|
||||||
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
|
|||||||
|
|
||||||
def set_agent
|
def set_agent
|
||||||
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
@agent = Current.account.users.find_by(id: params[:assignee_id])
|
||||||
@conversation.update_assignee(@agent)
|
@conversation.assignee = @agent
|
||||||
|
@conversation.save!
|
||||||
render_agent
|
render_agent
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def retry
|
||||||
|
return if message.blank?
|
||||||
|
|
||||||
|
message.update!(status: :sent, content_attributes: {})
|
||||||
|
::SendReplyJob.perform_later(message.id)
|
||||||
|
rescue StandardError => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
def translate
|
def translate
|
||||||
return head :ok if already_translated_content_available?
|
return head :ok if already_translated_content_available?
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status]
|
if params[:status].present?
|
||||||
set_conversation_status
|
set_conversation_status
|
||||||
@status = @conversation.save!
|
@status = @conversation.save!
|
||||||
else
|
else
|
||||||
@@ -110,8 +110,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assign_conversation
|
def assign_conversation
|
||||||
@agent = Current.account.users.find(current_user.id)
|
@conversation.assignee = current_user
|
||||||
@conversation.update_assignee(@agent)
|
@conversation.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||||
RESULTS_PER_PAGE = 15
|
RESULTS_PER_PAGE = 15
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :fetch_notification, only: [:update]
|
before_action :fetch_notification, only: [:update, :destroy, :snooze]
|
||||||
before_action :set_primary_actor, only: [:read_all]
|
before_action :set_primary_actor, only: [:read_all]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
@@ -28,11 +29,21 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
|||||||
render json: @notification
|
render json: @notification
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@notification.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def unread_count
|
def unread_count
|
||||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||||
render json: @unread_count
|
render json: @unread_count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snooze
|
||||||
|
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
|
||||||
|
render json: @notification
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_primary_actor
|
def set_primary_actor
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def logo
|
||||||
|
@portal.logo.purge if @portal.logo.attached?
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def process_attached_logo
|
def process_attached_logo
|
||||||
blob_id = params[:blob_id]
|
blob_id = params[:blob_id]
|
||||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||||||
sender: @contact,
|
sender: @contact,
|
||||||
content: permitted_params[:message][:content],
|
content: permitted_params[:message][:content],
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
|
content_attributes: {
|
||||||
|
in_reply_to: permitted_params[:message][:reply_to]
|
||||||
|
},
|
||||||
echo_id: permitted_params[:message][:echo_id],
|
echo_id: permitted_params[:message][:echo_id],
|
||||||
message_type: :incoming
|
message_type: :incoming
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
conversation.contact_last_seen_at = DateTime.now.utc
|
conversation.contact_last_seen_at = DateTime.now.utc
|
||||||
conversation.save!
|
conversation.save!
|
||||||
|
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_typing
|
def toggle_typing
|
||||||
head :ok && return if conversation.nil?
|
|
||||||
|
|
||||||
case permitted_params[:typing_status]
|
case permitted_params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
@@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
return head :not_found if conversation.nil?
|
|
||||||
|
|
||||||
return head :forbidden unless @web_widget.end_conversation?
|
return head :forbidden unless @web_widget.end_conversation?
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
@@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_not_found_if_empty
|
||||||
|
return head :not_found if conversation.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
# timestamp parameter is used in create conversation method
|
# timestamp parameter is used in create conversation method
|
||||||
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_message
|
def set_message
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = AgentBot.new(agent_bot_params)
|
@resource = AgentBot.new(agent_bot_params.except(:avatar_url))
|
||||||
@resource.save!
|
@resource.save!
|
||||||
|
process_avatar_from_url
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(agent_bot_params)
|
@resource.update!(agent_bot_params.except(:avatar_url))
|
||||||
|
process_avatar_from_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@@ -23,6 +25,11 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def avatar
|
||||||
|
@resource.avatar.purge if @resource.avatar.attached?
|
||||||
|
@resource
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_resource
|
def set_resource
|
||||||
@@ -30,6 +37,10 @@ class Platform::Api::V1::AgentBotsController < PlatformController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def agent_bot_params
|
def agent_bot_params
|
||||||
params.permit(:name, :description, :account_id, :outgoing_url)
|
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_avatar_from_url
|
||||||
|
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
def update_last_seen
|
def update_last_seen
|
||||||
@conversation.contact_last_seen_at = DateTime.now.utc
|
@conversation.contact_last_seen_at = DateTime.now.utc
|
||||||
@conversation.save!
|
@conversation.save!
|
||||||
|
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
|
||||||
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query, :locale, :sort, :status)
|
params.permit(:query, :locale, :sort, :status)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_color_scheme
|
def set_color_scheme
|
||||||
@theme = if %w[dark light].include?(params[:theme])
|
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme])
|
||||||
params[:theme]
|
end
|
||||||
else
|
|
||||||
''
|
def portal
|
||||||
end
|
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_locale(&)
|
def set_locale(&)
|
||||||
@@ -40,6 +40,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
|
|
||||||
def switch_locale_with_article(&)
|
def switch_locale_with_article(&)
|
||||||
article = Article.find_by(slug: params[:article_slug])
|
article = Article.find_by(slug: params[:article_slug])
|
||||||
|
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
|
||||||
|
render_404 && return if article.blank?
|
||||||
|
|
||||||
@locale = if article.category.present?
|
@locale = if article.category.present?
|
||||||
article.category.locale
|
article.category.locale
|
||||||
@@ -53,4 +55,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
def allow_iframe_requests
|
def allow_iframe_requests
|
||||||
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_404
|
||||||
|
portal
|
||||||
|
render 'public/api/v1/portals/error/404', status: :not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
|
||||||
end
|
|
||||||
|
|
||||||
def portal
|
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
|
||||||
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
render_404 && return if @category.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,21 +1,38 @@
|
|||||||
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||||
|
before_action :set_config
|
||||||
|
before_action :allowed_configs
|
||||||
def show
|
def show
|
||||||
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
|
||||||
# ref: https://github.com/rubocop/rubocop/issues/7767
|
# ref: https://github.com/rubocop/rubocop/issues/7767
|
||||||
# rubocop:disable Style/HashTransformValues
|
# rubocop:disable Style/HashTransformValues
|
||||||
@fb_config = InstallationConfig.where(name: @allowed_configs)
|
@app_config = InstallationConfig.where(name: @allowed_configs)
|
||||||
.pluck(:name, :serialized_value)
|
.pluck(:name, :serialized_value)
|
||||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||||
.to_h
|
.to_h
|
||||||
# rubocop:enable Style/HashTransformValues
|
# rubocop:enable Style/HashTransformValues
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
params['app_config'].each do |key, value|
|
params['app_config'].each do |key, value|
|
||||||
|
next unless @allowed_configs.include?(key)
|
||||||
|
|
||||||
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
|
||||||
i.value = value
|
i.value = value
|
||||||
i.save!
|
i.save!
|
||||||
end
|
end
|
||||||
redirect_to super_admin_app_config_url
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_config
|
||||||
|
@config = params[:config]
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_configs
|
||||||
|
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
|
||||||
|
|||||||
@@ -20,4 +20,13 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
|
|||||||
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
params.fetch(resource_name, {}).fetch(:direction, 'desc')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invalid_action_perfomed
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
flash[:error] = 'Invalid action performed'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
true
|
true
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@error_message = e.message
|
Rails.logger.error e.message
|
||||||
|
@error_message = 'Invalid credentials. Please try again.'
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
|
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
|
||||||
|
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
|
||||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||||
# For example, you may want to send an email after a foo is updated.
|
# For example, you may want to send an email after a foo is updated.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def refresh
|
||||||
|
Internal::CheckNewVersionsJob.perform_now
|
||||||
|
# rubocop:disable Rails/I18nLocaleTexts
|
||||||
|
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
|
||||||
|
# rubocop:enable Rails/I18nLocaleTexts
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
class Twilio::DeliveryStatusController < ApplicationController
|
||||||
|
def create
|
||||||
|
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(
|
||||||
|
:AccountSid,
|
||||||
|
:From,
|
||||||
|
:MessageSid,
|
||||||
|
:MessagingServiceSid,
|
||||||
|
:MessageStatus,
|
||||||
|
:ErrorCode,
|
||||||
|
:ErrorMessage
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -30,11 +30,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
|
|||||||
# SHOW_PAGE_ATTRIBUTES
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed on the model's show page.
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
SHOW_PAGE_ATTRIBUTES = %i[
|
SHOW_PAGE_ATTRIBUTES = %i[
|
||||||
owner
|
|
||||||
id
|
|
||||||
token
|
token
|
||||||
created_at
|
|
||||||
updated_at
|
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
outgoing_url
|
outgoing_url
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class PlatformAppDashboard < Administrate::BaseDashboard
|
|||||||
name
|
name
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
updated_at: Field::DateTime,
|
updated_at: Field::DateTime,
|
||||||
pubsub_token: Field::String,
|
pubsub_token: Field::String,
|
||||||
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
|
||||||
accounts: CountField
|
accounts: CountField,
|
||||||
|
access_token: Field::HasOne
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
# COLLECTION_ATTRIBUTES
|
# COLLECTION_ATTRIBUTES
|
||||||
@@ -67,6 +68,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||||||
updated_at
|
updated_at
|
||||||
confirmed_at
|
confirmed_at
|
||||||
account_users
|
account_users
|
||||||
|
access_token
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
|
|||||||
@@ -18,4 +18,9 @@ class ContactDrop < BaseDrop
|
|||||||
def last_name
|
def last_name
|
||||||
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attribute
|
||||||
|
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||||
|
custom_attributes.transform_keys(&:to_s)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class ConversationDrop < BaseDrop
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_attribute
|
||||||
|
custom_attributes = @obj.try(:custom_attributes) || {}
|
||||||
|
custom_attributes.transform_keys(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def message_sender_name(sender)
|
def message_sender_name(sender)
|
||||||
|
|||||||
@@ -3,13 +3,21 @@ class ConversationFinder
|
|||||||
|
|
||||||
DEFAULT_STATUS = 'open'.freeze
|
DEFAULT_STATUS = 'open'.freeze
|
||||||
SORT_OPTIONS = {
|
SORT_OPTIONS = {
|
||||||
latest: 'latest',
|
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
|
||||||
sort_on_created_at: 'sort_on_created_at',
|
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
|
||||||
last_user_message_at: 'last_user_message_at',
|
'created_at_asc' => %w[sort_on_created_at asc],
|
||||||
sort_on_priority: 'sort_on_priority',
|
'created_at_desc' => %w[sort_on_created_at desc],
|
||||||
sort_on_waiting_since: 'sort_on_waiting_since'
|
'priority_asc' => %w[sort_on_priority asc],
|
||||||
}.with_indifferent_access
|
'priority_desc' => %w[sort_on_priority desc],
|
||||||
|
'waiting_since_asc' => %w[sort_on_waiting_since asc],
|
||||||
|
'waiting_since_desc' => %w[sort_on_waiting_since desc],
|
||||||
|
|
||||||
|
# To be removed in v3.5.0
|
||||||
|
'latest' => %w[sort_on_last_activity_at desc],
|
||||||
|
'sort_on_created_at' => %w[sort_on_created_at asc],
|
||||||
|
'sort_on_priority' => %w[sort_on_priority desc],
|
||||||
|
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
|
||||||
|
}.with_indifferent_access
|
||||||
# assumptions
|
# assumptions
|
||||||
# inbox_id if not given, take from all conversations, else specific to inbox
|
# inbox_id if not given, take from all conversations, else specific to inbox
|
||||||
# assignee_type if not given, take 'all'
|
# assignee_type if not given, take 'all'
|
||||||
@@ -159,7 +167,8 @@ class ConversationFinder
|
|||||||
@conversations = @conversations.includes(
|
@conversations = @conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||||
)
|
)
|
||||||
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
|
|
||||||
@conversations.send(sort_by).page(current_page)
|
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
|
||||||
|
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,71 @@
|
|||||||
module PortalHelper
|
module PortalHelper
|
||||||
def generate_portal_bg_color(portal_color, theme)
|
def generate_portal_bg_color(portal_color, theme)
|
||||||
base_color = theme == 'dark' ? 'black' : 'white'
|
base_color = theme == 'dark' ? 'black' : 'white'
|
||||||
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
|
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_portal_bg(portal_color, theme)
|
def generate_portal_bg(portal_color, theme)
|
||||||
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
|
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
|
||||||
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_gradient_to_bottom(theme)
|
||||||
|
base_color = theme == 'dark' ? '#151718' : 'white'
|
||||||
|
"linear-gradient(to bottom, transparent, #{base_color})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_portal_hover_color(portal_color, theme)
|
||||||
|
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
|
||||||
|
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def language_name(locale)
|
||||||
|
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
|
||||||
|
language_map[locale] || locale
|
||||||
|
end
|
||||||
|
|
||||||
|
def theme_query_string(theme)
|
||||||
|
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
|
||||||
|
if is_plain_layout_enabled
|
||||||
|
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
|
||||||
|
else
|
||||||
|
"/hc/#{portal_slug}/#{portal_locale}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_category_link(params)
|
||||||
|
portal_slug = params[:portal_slug]
|
||||||
|
category_locale = params[:category_locale]
|
||||||
|
category_slug = params[:category_slug]
|
||||||
|
theme = params[:theme]
|
||||||
|
is_plain_layout_enabled = params[:is_plain_layout_enabled]
|
||||||
|
|
||||||
|
if is_plain_layout_enabled
|
||||||
|
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
|
||||||
|
else
|
||||||
|
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
|
||||||
|
if is_plain_layout_enabled
|
||||||
|
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
|
||||||
|
else
|
||||||
|
"/hc/#{portal_slug}/articles/#{article_slug}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_category_content(content)
|
||||||
|
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
|
||||||
|
end
|
||||||
|
|
||||||
|
def thumbnail_bg_color(username)
|
||||||
|
colors = ['#6D95BA', '#A4C3C3', '#E19191']
|
||||||
|
return colors.sample if username.blank?
|
||||||
|
|
||||||
|
colors[username.length % colors.size]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching"
|
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||||
id="app"
|
id="app"
|
||||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
|
<template v-if="currentAccountId">
|
||||||
<payment-pending-banner />
|
<payment-pending-banner />
|
||||||
<upgrade-banner />
|
<upgrade-banner />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ export default {
|
|||||||
return axios.delete(endPoints('deleteAvatar').url);
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetPassword({ email }) {
|
||||||
|
const urlData = endPoints('resetPassword');
|
||||||
|
return axios.post(urlData.url, { email });
|
||||||
|
},
|
||||||
|
|
||||||
setActiveAccount({ accountId }) {
|
setActiveAccount({ accountId }) {
|
||||||
const urlData = endPoints('setActiveAccount');
|
const urlData = endPoints('setActiveAccount');
|
||||||
return axios.put(urlData.url, {
|
return axios.put(urlData.url, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* global axios */
|
/* global axios */
|
||||||
|
|
||||||
import PortalsAPI from './portals';
|
import PortalsAPI from './portals';
|
||||||
|
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
|
||||||
|
|
||||||
class ArticlesAPI extends PortalsAPI {
|
class ArticlesAPI extends PortalsAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
|
|||||||
portalSlug,
|
portalSlug,
|
||||||
locale,
|
locale,
|
||||||
status,
|
status,
|
||||||
author_id,
|
authorId,
|
||||||
category_slug,
|
categorySlug,
|
||||||
|
sort,
|
||||||
}) {
|
}) {
|
||||||
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
|
const url = getArticleSearchURL({
|
||||||
if (status !== undefined) baseUrl += `&status=${status}`;
|
pageNumber,
|
||||||
if (author_id) baseUrl += `&author_id=${author_id}`;
|
portalSlug,
|
||||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
locale,
|
||||||
return axios.get(baseUrl);
|
status,
|
||||||
|
authorId,
|
||||||
|
categorySlug,
|
||||||
|
sort,
|
||||||
|
host: this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
return axios.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchArticles({ portalSlug, query }) {
|
||||||
|
const url = getArticleSearchURL({
|
||||||
|
portalSlug,
|
||||||
|
query,
|
||||||
|
host: this.url,
|
||||||
|
});
|
||||||
|
return axios.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
getArticle({ id, portalSlug }) {
|
getArticle({ id, portalSlug }) {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
|
|||||||
deletePortal(portalSlug) {
|
deletePortal(portalSlug) {
|
||||||
return axios.delete(`${this.url}/${portalSlug}`);
|
return axios.delete(`${this.url}/${portalSlug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteLogo(portalSlug) {
|
||||||
|
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PortalsAPI;
|
export default PortalsAPI;
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ export const buildCreatePayload = ({
|
|||||||
payload.append('echo_id', echoId);
|
payload.append('echo_id', echoId);
|
||||||
payload.append('cc_emails', ccEmails);
|
payload.append('cc_emails', ccEmails);
|
||||||
payload.append('bcc_emails', bccEmails);
|
payload.append('bcc_emails', bccEmails);
|
||||||
|
|
||||||
if (toEmails) {
|
if (toEmails) {
|
||||||
payload.append('to_emails', toEmails);
|
payload.append('to_emails', toEmails);
|
||||||
}
|
}
|
||||||
|
if (contentAttributes) {
|
||||||
|
payload.append('content_attributes', JSON.stringify(contentAttributes));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
content: message,
|
content: message,
|
||||||
@@ -82,6 +86,12 @@ class MessageApi extends ApiClient {
|
|||||||
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry(conversationID, messageId) {
|
||||||
|
return axios.post(
|
||||||
|
`${this.url}/${conversationID}/messages/${messageId}/retry`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getPreviousMessages({ conversationId, after, before }) {
|
getPreviousMessages({ conversationId, after, before }) {
|
||||||
const params = { before };
|
const params = { before };
|
||||||
if (after && Number(after) !== Number(before)) {
|
if (after && Number(after) !== Number(before)) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
|
|||||||
portalSlug: 'room-rental',
|
portalSlug: 'room-rental',
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
status: 'published',
|
status: 'published',
|
||||||
author_id: '1',
|
authorId: '1',
|
||||||
});
|
});
|
||||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
|
||||||
@@ -85,6 +85,33 @@ describe('#PortalAPI', () => {
|
|||||||
window.axios = originalAxios;
|
window.axios = originalAxios;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#searchArticles', () => {
|
||||||
|
articlesAPI.searchArticles({
|
||||||
|
query: 'test',
|
||||||
|
portalSlug: 'room-rental',
|
||||||
|
});
|
||||||
|
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/portals/room-rental/articles?query=test'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('API calls', () => {
|
||||||
|
const originalAxios = window.axios;
|
||||||
|
const axiosMock = {
|
||||||
|
post: jest.fn(() => Promise.resolve()),
|
||||||
|
get: jest.fn(() => Promise.resolve()),
|
||||||
|
patch: jest.fn(() => Promise.resolve()),
|
||||||
|
delete: jest.fn(() => Promise.resolve()),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.axios = axiosMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.axios = originalAxios;
|
||||||
|
});
|
||||||
|
|
||||||
it('#updateArticle', () => {
|
it('#updateArticle', () => {
|
||||||
articlesAPI.updateArticle({
|
articlesAPI.updateArticle({
|
||||||
articleId: 1,
|
articleId: 1,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
|
|||||||
message: 'test content',
|
message: 'test content',
|
||||||
echoId: 12,
|
echoId: 12,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
|
contentAttributes: { in_reply_to: 12 },
|
||||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||||
});
|
});
|
||||||
expect(formPayload).toBeInstanceOf(FormData);
|
expect(formPayload).toBeInstanceOf(FormData);
|
||||||
@@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
|
|||||||
expect(formPayload.get('echo_id')).toEqual('12');
|
expect(formPayload.get('echo_id')).toEqual('12');
|
||||||
expect(formPayload.get('private')).toEqual('true');
|
expect(formPayload.get('private')).toEqual('true');
|
||||||
expect(formPayload.get('cc_emails')).toEqual('');
|
expect(formPayload.get('cc_emails')).toEqual('');
|
||||||
|
expect(formPayload.get('bcc_emails')).toEqual('');
|
||||||
|
expect(formPayload.get('content_attributes')).toEqual(
|
||||||
|
'{"in_reply_to":12}'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds object payload if file is not available', () => {
|
it('builds object payload if file is not available', () => {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
@@ -1,9 +1,5 @@
|
|||||||
@import '~vue2-datepicker/scss/index';
|
@import '~vue2-datepicker/scss/index';
|
||||||
|
|
||||||
.mx-datepicker-popup {
|
|
||||||
@apply z-[99999];
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
&.no-margin {
|
&.no-margin {
|
||||||
.mx-input {
|
.mx-input {
|
||||||
@@ -29,10 +25,52 @@
|
|||||||
.mx-input[readonly] {
|
.mx-input[readonly] {
|
||||||
@apply bg-white dark:bg-slate-900 cursor-pointer;
|
@apply bg-white dark:bg-slate-900 cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx-icon-calendar {
|
||||||
|
@apply dark:text-slate-500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-calendar-content .cell:hover {
|
.mx-datepicker-main {
|
||||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
@apply border-0 bg-white dark:bg-slate-800;
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
&.disabled {
|
||||||
|
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.hover-in-range,
|
||||||
|
&.in-range {
|
||||||
|
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-time {
|
||||||
|
@apply border-0 bg-white dark:bg-slate-800;
|
||||||
|
|
||||||
|
.mx-time-header {
|
||||||
|
@apply border-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-time-item {
|
||||||
|
&.disabled {
|
||||||
|
@apply bg-slate-25 dark:bg-slate-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-slate-75 dark:bg-slate-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.today {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-datepicker-popup {
|
||||||
|
@apply z-[99999];
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-datepicker-inline {
|
.mx-datepicker-inline {
|
||||||
@@ -41,24 +79,4 @@
|
|||||||
.mx-calendar {
|
.mx-calendar {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell.disabled {
|
|
||||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-time-item.disabled {
|
|
||||||
@apply bg-slate-25 dark:bg-slate-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.today {
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-datepicker-main {
|
|
||||||
@apply border-0 bg-white dark:bg-slate-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-time-header {
|
|
||||||
@apply border-0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,23 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-hooks {
|
||||||
|
.formulate-input[data-type='checkbox'] {
|
||||||
|
.formulate-input-wrapper {
|
||||||
|
@apply flex;
|
||||||
|
|
||||||
|
.formulate-input-element {
|
||||||
|
@apply pr-2;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.formulate-input-element-decorator {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,39 +1,8 @@
|
|||||||
@import '../variables';
|
@import 'shared/assets/fonts/inter';
|
||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
.superadmin-body {
|
body {
|
||||||
background: var(--color-background);
|
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
|
||||||
.hero--title {
|
|
||||||
font-size: var(--font-size-mega);
|
|
||||||
font-weight: var(--font-weight-light);
|
|
||||||
margin-top: var(--space-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-subscription--checkbox {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
input {
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-right: var(--space-one);
|
|
||||||
margin-top: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-box {
|
|
||||||
background-color: var(--r-500);
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--color-white);
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
@import 'shared/assets/fonts/plus-jakarta';
|
|
||||||
@import '../variables';
|
|
||||||
@import '~shared/assets/stylesheets/ionicons';
|
|
||||||
@@ -34,26 +34,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.hollow {
|
&.hollow {
|
||||||
@apply border border-slate-200 dark:border-slate-600 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900;
|
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
|
||||||
|
|
||||||
&.secondary {
|
&.secondary {
|
||||||
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
|
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
@apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800;
|
@apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.alert {
|
&.alert {
|
||||||
@apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800;
|
@apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
@apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800;
|
@apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
|
@apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400;
|
||||||
|
|
||||||
&.secondary {
|
&.secondary {
|
||||||
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
|
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,10 @@
|
|||||||
|
|
||||||
&.is-image {
|
&.is-image {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
|
|
||||||
|
.message__mail-head {
|
||||||
|
@apply px-4 py-2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
|
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal--close {
|
|
||||||
@apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-top-bar {
|
.page-top-bar {
|
||||||
@apply px-8 pt-9 pb-0;
|
@apply px-8 pt-9 pb-0;
|
||||||
|
|
||||||
|
|||||||
@@ -126,49 +126,33 @@
|
|||||||
@assign-team="onAssignTeamsForBulk"
|
@assign-team="onAssignTeamsForBulk"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="activeConversation"
|
ref="conversationList"
|
||||||
class="conversations-list flex-1"
|
class="conversations-list flex-1"
|
||||||
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
||||||
>
|
>
|
||||||
<div>
|
<virtual-list
|
||||||
<conversation-card
|
ref="conversationVirtualList"
|
||||||
v-for="chat in conversationList"
|
:data-key="'id'"
|
||||||
:key="chat.id"
|
:data-sources="conversationList"
|
||||||
:active-label="label"
|
:data-component="itemComponent"
|
||||||
:team-id="teamId"
|
:extra-props="virtualListExtraProps"
|
||||||
:folders-id="foldersId"
|
class="w-full overflow-auto h-full"
|
||||||
:chat="chat"
|
footer-tag="div"
|
||||||
:conversation-type="conversationType"
|
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
|
||||||
:selected="isConversationSelected(chat.id)"
|
|
||||||
@select-conversation="selectConversation"
|
|
||||||
@de-select-conversation="deSelectConversation"
|
|
||||||
@assign-agent="onAssignAgent"
|
|
||||||
@assign-team="onAssignTeam"
|
|
||||||
@assign-label="onAssignLabels"
|
|
||||||
@update-conversation-status="toggleConversationStatus"
|
|
||||||
@context-menu-toggle="onContextMenuToggle"
|
|
||||||
@mark-as-unread="markAsUnread"
|
|
||||||
@assign-priority="assignPriority"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
|
||||||
<span class="spinner mt-4 mb-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<woot-button
|
|
||||||
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
|
||||||
variant="clear"
|
|
||||||
size="expanded"
|
|
||||||
class="load-more--button"
|
|
||||||
@click="loadMoreConversations"
|
|
||||||
>
|
>
|
||||||
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
<template #footer>
|
||||||
</woot-button>
|
<div v-if="chatListLoading" class="text-center">
|
||||||
|
<span class="spinner mt-4 mb-4" />
|
||||||
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
|
</div>
|
||||||
{{ $t('CHAT_LIST.EOF') }}
|
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
|
||||||
</p>
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
|
</p>
|
||||||
|
<intersection-observer
|
||||||
|
v-if="!showEndOfListMessage && !chatListLoading"
|
||||||
|
:options="infiniteLoaderOptions"
|
||||||
|
@observed="loadMoreConversations"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</virtual-list>
|
||||||
</div>
|
</div>
|
||||||
<woot-modal
|
<woot-modal
|
||||||
:show.sync="showAdvancedFilters"
|
:show.sync="showAdvancedFilters"
|
||||||
@@ -191,11 +175,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import VirtualList from 'vue-virtual-scroll-list';
|
||||||
|
|
||||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
import ConversationCard from './widgets/conversation/ConversationCard.vue';
|
import ConversationItem from './ConversationItem.vue';
|
||||||
import timeMixin from '../mixins/time';
|
import timeMixin from '../mixins/time';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
@@ -207,6 +192,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
|||||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import filterMixin from 'shared/mixins/filterMixin';
|
import filterMixin from 'shared/mixins/filterMixin';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||||
import countries from 'shared/constants/countries';
|
import countries from 'shared/constants/countries';
|
||||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||||
@@ -221,16 +207,20 @@ import {
|
|||||||
isOnUnattendedView,
|
isOnUnattendedView,
|
||||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||||
|
import IntersectionObserver from './IntersectionObserver.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AddCustomViews,
|
AddCustomViews,
|
||||||
ChatTypeTabs,
|
ChatTypeTabs,
|
||||||
ConversationCard,
|
// eslint-disable-next-line vue/no-unused-components
|
||||||
|
ConversationItem,
|
||||||
ConversationAdvancedFilter,
|
ConversationAdvancedFilter,
|
||||||
DeleteCustomViews,
|
DeleteCustomViews,
|
||||||
ConversationBulkActions,
|
ConversationBulkActions,
|
||||||
ConversationBasicFilter,
|
ConversationBasicFilter,
|
||||||
|
IntersectionObserver,
|
||||||
|
VirtualList,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
timeMixin,
|
timeMixin,
|
||||||
@@ -238,7 +228,22 @@ export default {
|
|||||||
eventListenerMixins,
|
eventListenerMixins,
|
||||||
alertMixin,
|
alertMixin,
|
||||||
filterMixin,
|
filterMixin,
|
||||||
|
uiSettingsMixin,
|
||||||
],
|
],
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
// Actions to be performed on virtual list item and context menu.
|
||||||
|
selectConversation: this.selectConversation,
|
||||||
|
deSelectConversation: this.deSelectConversation,
|
||||||
|
assignAgent: this.onAssignAgent,
|
||||||
|
assignTeam: this.onAssignTeam,
|
||||||
|
assignLabels: this.onAssignLabels,
|
||||||
|
updateConversationStatus: this.toggleConversationStatus,
|
||||||
|
toggleContextMenu: this.onContextMenuToggle,
|
||||||
|
markAsUnread: this.markAsUnread,
|
||||||
|
assignPriority: this.assignPriority,
|
||||||
|
};
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -273,7 +278,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
activeStatus: wootConstants.STATUS_TYPE.OPEN,
|
||||||
activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
|
activeSortBy: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC,
|
||||||
showAdvancedFilters: false,
|
showAdvancedFilters: false,
|
||||||
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
advancedFilterTypes: advancedFilterTypes.map(filter => ({
|
||||||
...filter,
|
...filter,
|
||||||
@@ -289,6 +294,21 @@ export default {
|
|||||||
selectedInboxes: [],
|
selectedInboxes: [],
|
||||||
isContextMenuOpen: false,
|
isContextMenuOpen: false,
|
||||||
appliedFilter: [],
|
appliedFilter: [],
|
||||||
|
infiniteLoaderOptions: {
|
||||||
|
root: this.$refs.conversationList,
|
||||||
|
rootMargin: '100px 0px 100px 0px',
|
||||||
|
},
|
||||||
|
|
||||||
|
itemComponent: ConversationItem,
|
||||||
|
// virtualListExtraProps is to pass the props to the conversationItem component.
|
||||||
|
virtualListExtraProps: {
|
||||||
|
label: this.label,
|
||||||
|
teamId: this.teamId,
|
||||||
|
foldersId: this.foldersId,
|
||||||
|
conversationType: this.conversationType,
|
||||||
|
showAssignee: false,
|
||||||
|
isConversationSelected: this.isConversationSelected,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -408,8 +428,12 @@ export default {
|
|||||||
},
|
},
|
||||||
conversationListPagination() {
|
conversationListPagination() {
|
||||||
const conversationsPerPage = 25;
|
const conversationsPerPage = 25;
|
||||||
|
const hasChatsOnView =
|
||||||
|
this.chatsOnView &&
|
||||||
|
Array.isArray(this.chatsOnView) &&
|
||||||
|
!this.chatsOnView.length;
|
||||||
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
||||||
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
|
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
|
||||||
const isUnderPerPage =
|
const isUnderPerPage =
|
||||||
this.chatsOnView.length < conversationsPerPage &&
|
this.chatsOnView.length < conversationsPerPage &&
|
||||||
this.activeAssigneeTabCount < conversationsPerPage &&
|
this.activeAssigneeTabCount < conversationsPerPage &&
|
||||||
@@ -503,18 +527,25 @@ export default {
|
|||||||
},
|
},
|
||||||
label() {
|
label() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('label', this.label);
|
||||||
},
|
},
|
||||||
conversationType() {
|
conversationType() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('conversationType', this.conversationType);
|
||||||
},
|
},
|
||||||
activeFolder() {
|
activeFolder() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
|
this.updateVirtualListProps('foldersId', this.foldersId);
|
||||||
},
|
},
|
||||||
chatLists() {
|
chatLists() {
|
||||||
this.chatsOnView = this.conversationList;
|
this.chatsOnView = this.conversationList;
|
||||||
},
|
},
|
||||||
|
showAssigneeInConversationCard(newVal) {
|
||||||
|
this.updateVirtualListProps('showAssignee', newVal);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.setFiltersFromUISettings();
|
||||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
@@ -528,6 +559,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateVirtualListProps(key, value) {
|
||||||
|
this.virtualListExtraProps = {
|
||||||
|
...this.virtualListExtraProps,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
},
|
||||||
onApplyFilter(payload) {
|
onApplyFilter(payload) {
|
||||||
this.resetBulkActions();
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
@@ -544,6 +581,15 @@ export default {
|
|||||||
this.$store.dispatch('customViews/update', payloadData);
|
this.$store.dispatch('customViews/update', payloadData);
|
||||||
this.closeAdvanceFiltersModal();
|
this.closeAdvanceFiltersModal();
|
||||||
},
|
},
|
||||||
|
setFiltersFromUISettings() {
|
||||||
|
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
|
||||||
|
const { status, order_by: orderBy } = filterBy;
|
||||||
|
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
|
||||||
|
this.activeSortBy =
|
||||||
|
Object.keys(wootConstants.SORT_BY_TYPE).find(
|
||||||
|
sortField => sortField === orderBy
|
||||||
|
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
||||||
|
},
|
||||||
onClickOpenAddFoldersModal() {
|
onClickOpenAddFoldersModal() {
|
||||||
this.showAddFoldersModal = true;
|
this.showAddFoldersModal = true;
|
||||||
},
|
},
|
||||||
@@ -622,10 +668,10 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
getKeyboardListenerParams() {
|
getKeyboardListenerParams() {
|
||||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
const allConversations = this.$refs.conversationList.querySelectorAll(
|
||||||
'div.conversations-list div.conversation'
|
'div.conversations-list div.conversation'
|
||||||
);
|
);
|
||||||
const activeConversation = this.$refs.activeConversation.querySelector(
|
const activeConversation = this.$refs.conversationList.querySelector(
|
||||||
'div.conversations-list div.conversation.active'
|
'div.conversations-list div.conversation.active'
|
||||||
);
|
);
|
||||||
const activeConversationIndex = [...allConversations].indexOf(
|
const activeConversationIndex = [...allConversations].indexOf(
|
||||||
@@ -681,9 +727,12 @@ export default {
|
|||||||
fetchConversations() {
|
fetchConversations() {
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('fetchAllConversations', this.conversationFilters)
|
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
},
|
},
|
||||||
loadMoreConversations() {
|
loadMoreConversations() {
|
||||||
|
if (this.hasCurrentPageEndReached || this.chatListLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.hasAppliedFiltersOrActiveFolders) {
|
if (!this.hasAppliedFiltersOrActiveFolders) {
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
}
|
}
|
||||||
@@ -702,7 +751,7 @@ export default {
|
|||||||
queryData: filterQueryGenerator(payload),
|
queryData: filterQueryGenerator(payload),
|
||||||
page,
|
page,
|
||||||
})
|
})
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
this.showAdvancedFilters = false;
|
this.showAdvancedFilters = false;
|
||||||
},
|
},
|
||||||
fetchSavedFilteredConversations(payload) {
|
fetchSavedFilteredConversations(payload) {
|
||||||
@@ -712,7 +761,7 @@ export default {
|
|||||||
queryData: payload,
|
queryData: payload,
|
||||||
page,
|
page,
|
||||||
})
|
})
|
||||||
.then(() => this.$emit('conversation-load'));
|
.then(this.emitConversationLoaded);
|
||||||
},
|
},
|
||||||
updateAssigneeTab(selectedTab) {
|
updateAssigneeTab(selectedTab) {
|
||||||
if (this.activeAssigneeTab !== selectedTab) {
|
if (this.activeAssigneeTab !== selectedTab) {
|
||||||
@@ -724,6 +773,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emitConversationLoaded() {
|
||||||
|
this.$emit('conversation-load');
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Addressing a known issue in the virtual list library where dynamically added items
|
||||||
|
// might not render correctly. This workaround involves a slight manual adjustment
|
||||||
|
// to the scroll position, triggering the list to refresh its rendering.
|
||||||
|
const virtualList = this.$refs.conversationVirtualList;
|
||||||
|
const scrollToOffset = virtualList?.scrollToOffset;
|
||||||
|
const currentOffset = virtualList?.getOffset() || 0;
|
||||||
|
if (scrollToOffset) {
|
||||||
|
scrollToOffset(currentOffset + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
resetBulkActions() {
|
resetBulkActions() {
|
||||||
this.selectedConversations = [];
|
this.selectedConversations = [];
|
||||||
this.selectedInboxes = [];
|
this.selectedInboxes = [];
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<conversation-card
|
||||||
|
:key="source.id"
|
||||||
|
:active-label="label"
|
||||||
|
:team-id="teamId"
|
||||||
|
:folders-id="foldersId"
|
||||||
|
:chat="source"
|
||||||
|
:conversation-type="conversationType"
|
||||||
|
:selected="isConversationSelected(source.id)"
|
||||||
|
:show-assignee="showAssignee"
|
||||||
|
:enable-context-menu="true"
|
||||||
|
@select-conversation="selectConversation"
|
||||||
|
@de-select-conversation="deSelectConversation"
|
||||||
|
@assign-agent="assignAgent"
|
||||||
|
@assign-team="assignTeam"
|
||||||
|
@assign-label="assignLabels"
|
||||||
|
@update-conversation-status="updateConversationStatus"
|
||||||
|
@context-menu-toggle="toggleContextMenu"
|
||||||
|
@mark-as-unread="markAsUnread"
|
||||||
|
@assign-priority="assignPriority"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConversationCard from './widgets/conversation/ConversationCard.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ConversationCard,
|
||||||
|
},
|
||||||
|
inject: [
|
||||||
|
'selectConversation',
|
||||||
|
'deSelectConversation',
|
||||||
|
'assignAgent',
|
||||||
|
'assignTeam',
|
||||||
|
'assignLabels',
|
||||||
|
'updateConversationStatus',
|
||||||
|
'toggleContextMenu',
|
||||||
|
'markAsUnread',
|
||||||
|
'assignPriority',
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
source: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
teamId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
conversationType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
foldersId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isConversationSelected: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
showAssignee: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="observedElement" class="h-6 w-full" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
this.registerInfiniteLoader();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unobserveInfiniteLoadObserver();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
registerInfiniteLoader() {
|
||||||
|
this.intersectionObserver = new IntersectionObserver(entries => {
|
||||||
|
if (entries && entries[0].isIntersecting) {
|
||||||
|
this.$emit('observed');
|
||||||
|
}
|
||||||
|
}, this.options);
|
||||||
|
this.intersectionObserver.observe(this.$refs.observedElement);
|
||||||
|
},
|
||||||
|
unobserveInfiniteLoadObserver() {
|
||||||
|
this.intersectionObserver.unobserve(this.$refs.observedElement);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -4,15 +4,19 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
:class="modalClassName"
|
:class="modalClassName"
|
||||||
transition="modal"
|
transition="modal"
|
||||||
@click="onBackDropClick"
|
@mousedown="handleMouseDown"
|
||||||
>
|
>
|
||||||
<div :class="modalContainerClassName" @click.stop>
|
<div
|
||||||
|
:class="modalContainerClassName"
|
||||||
|
@mouse.stop
|
||||||
|
@mousedown="event => event.stopPropagation()"
|
||||||
|
>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showCloseButton"
|
v-if="showCloseButton"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
icon="dismiss"
|
icon="dismiss"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
class="modal--close"
|
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
|
||||||
@click="close"
|
@click="close"
|
||||||
/>
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -50,6 +54,11 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mousedDownOnBackdrop: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
modalContainerClassName() {
|
modalContainerClassName() {
|
||||||
let className =
|
let className =
|
||||||
@@ -77,13 +86,22 @@ export default {
|
|||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseup', this.onMouseUp);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.body.removeEventListener('mouseup', this.onMouseUp);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleMouseDown() {
|
||||||
|
this.mousedDownOnBackdrop = true;
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.onClose();
|
this.onClose();
|
||||||
},
|
},
|
||||||
onBackDropClick() {
|
onMouseUp() {
|
||||||
if (this.closeOnBackdropClick) {
|
if (this.mousedDownOnBackdrop) {
|
||||||
|
this.mousedDownOnBackdrop = false;
|
||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@open-notification-panel="openNotificationPanel"
|
@open-notification-panel="openNotificationPanel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<secondary-sidebar
|
<secondary-sidebar
|
||||||
v-if="showSecondarySidebar"
|
v-if="showSecondarySidebar"
|
||||||
:class="sidebarClassName"
|
:class="sidebarClassName"
|
||||||
@@ -114,7 +113,12 @@ export default {
|
|||||||
if (!isAvailableForTheUser) {
|
if (!isAvailableForTheUser) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
menuItem.alwaysVisibleOnChatwootInstances &&
|
||||||
|
!this.isACustomBrandedInstance
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (menuItem.featureFlag) {
|
if (menuItem.featureFlag) {
|
||||||
return this.isFeatureEnabledonAccount(
|
return this.isFeatureEnabledonAccount(
|
||||||
this.accountId,
|
this.accountId,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const primaryMenuItems = accountId => [
|
|||||||
key: 'helpcenter',
|
key: 'helpcenter',
|
||||||
label: 'HELP_CENTER.TITLE',
|
label: 'HELP_CENTER.TITLE',
|
||||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||||
|
alwaysVisibleOnChatwootInstances: true,
|
||||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||||
toStateName: 'default_portal_articles',
|
toStateName: 'default_portal_articles',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export default {
|
|||||||
toStateName: 'labels_list',
|
toStateName: 'labels_list',
|
||||||
showModalForNewItem: true,
|
showModalForNewItem: true,
|
||||||
modalName: 'AddLabel',
|
modalName: 'AddLabel',
|
||||||
|
dataTestid: 'sidebar-new-label-button',
|
||||||
children: this.labels.map(label => ({
|
children: this.labels.map(label => ({
|
||||||
id: label.id,
|
id: label.id,
|
||||||
label: label.title,
|
label: label.title,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
icon="add"
|
icon="add"
|
||||||
|
:data-testid="menuItem.dataTestid"
|
||||||
@click="e => newLinkClick(e, navigate)"
|
@click="e => newLinkClick(e, navigate)"
|
||||||
>
|
>
|
||||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
|
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
|
||||||
}),
|
}),
|
||||||
isAICTAModalDismissed() {
|
isAICTAModalDismissed() {
|
||||||
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
return this.uiSettings.is_open_ai_cta_modal_dismissed;
|
||||||
@@ -70,7 +71,8 @@ export default {
|
|||||||
return (
|
return (
|
||||||
this.isAdmin &&
|
this.isAdmin &&
|
||||||
!this.isAIIntegrationEnabled &&
|
!this.isAIIntegrationEnabled &&
|
||||||
!this.isAICTAModalDismissed
|
!this.isAICTAModalDismissed &&
|
||||||
|
this.isAChatwootInstance
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
|
||||||
class="preview-item__wrap flex flex-col overflow-auto mt-4 max-h-[12.5rem]"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-for="(attachment, index) in attachments"
|
v-for="(attachment, index) in attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
class="preview-item flex p-1 bg-slate-50 dark:bg-slate-800 rounded-md w-[15rem] mb-1"
|
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
||||||
>
|
>
|
||||||
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||||
<img
|
<img
|
||||||
@@ -17,7 +15,7 @@
|
|||||||
📄
|
📄
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis ml-2">
|
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis">
|
||||||
<span
|
<span
|
||||||
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="configItem.type === 'frame' && configItem.url"
|
v-if="configItem.type === 'frame' && configItem.url"
|
||||||
:id="`dashboard-app--frame-${index}`"
|
:id="getFrameId(index)"
|
||||||
:src="configItem.url"
|
:src="configItem.url"
|
||||||
@load="() => onIframeLoad(index)"
|
@load="() => onIframeLoad(index)"
|
||||||
/>
|
/>
|
||||||
@@ -39,6 +39,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
position: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -82,10 +86,14 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getFrameId(index) {
|
||||||
|
return `dashboard-app--frame-${this.position}-${index}`;
|
||||||
|
},
|
||||||
onIframeLoad(index) {
|
onIframeLoad(index) {
|
||||||
const frameElement = document.getElementById(
|
// A possible alternative is to use ref instead of document.getElementById
|
||||||
`dashboard-app--frame-${index}`
|
// However, when ref is used together with v-for, the ref you get will be
|
||||||
);
|
// an array containing the child components mirroring the data source.
|
||||||
|
const frameElement = document.getElementById(this.getFrameId(index));
|
||||||
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||||
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||||
this.iframeLoading = false;
|
this.iframeLoading = false;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor-root">
|
<div ref="editorRoot" class="relative editor-root">
|
||||||
<tag-agents
|
<tag-agents
|
||||||
v-if="showUserMentions && isPrivate"
|
v-if="showUserMentions && isPrivate"
|
||||||
:search-key="mentionSearchKey"
|
:search-key="mentionSearchKey"
|
||||||
@@ -23,6 +23,23 @@
|
|||||||
@change="onFileChange"
|
@change="onFileChange"
|
||||||
/>
|
/>
|
||||||
<div ref="editor" />
|
<div ref="editor" />
|
||||||
|
<div
|
||||||
|
v-show="isImageNodeSelected && showImageResizeToolbar"
|
||||||
|
class="absolute shadow-md rounded-[4px] flex gap-1 py-1 px-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50"
|
||||||
|
:style="{
|
||||||
|
top: toolbarPosition.top,
|
||||||
|
left: toolbarPosition.left,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="size in sizes"
|
||||||
|
:key="size.name"
|
||||||
|
class="text-xs font-medium rounded-[4px] border border-solid border-slate-200 dark:border-slate-600 px-1.5 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||||
|
@click="setURLWithQueryAndImageSize(size)"
|
||||||
|
>
|
||||||
|
{{ size.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,6 +58,7 @@ import {
|
|||||||
suggestionsPlugin,
|
suggestionsPlugin,
|
||||||
triggerCharacters,
|
triggerCharacters,
|
||||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
import TagAgents from '../conversation/TagAgents.vue';
|
import TagAgents from '../conversation/TagAgents.vue';
|
||||||
import CannedResponse from '../conversation/CannedResponse.vue';
|
import CannedResponse from '../conversation/CannedResponse.vue';
|
||||||
@@ -48,6 +66,10 @@ import VariableList from '../conversation/VariableList.vue';
|
|||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
|
insertAtCursor,
|
||||||
|
scrollCursorIntoView,
|
||||||
|
findNodeToInsertImage,
|
||||||
|
setURLWithQueryAndSize,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
@@ -67,8 +89,10 @@ import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
|||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
|
import {
|
||||||
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||||
|
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
|
} from 'dashboard/constants/editor';
|
||||||
|
|
||||||
const createState = (
|
const createState = (
|
||||||
content,
|
content,
|
||||||
@@ -111,6 +135,8 @@ export default {
|
|||||||
// allowSignature is a kill switch, ensuring no signature methods
|
// allowSignature is a kill switch, ensuring no signature methods
|
||||||
// are triggered except when this flag is true
|
// are triggered except when this flag is true
|
||||||
allowSignature: { type: Boolean, default: false },
|
allowSignature: { type: Boolean, default: false },
|
||||||
|
channelType: { type: String, default: '' },
|
||||||
|
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -123,9 +149,16 @@ export default {
|
|||||||
editorView: null,
|
editorView: null,
|
||||||
range: null,
|
range: null,
|
||||||
state: undefined,
|
state: undefined,
|
||||||
|
isImageNodeSelected: false,
|
||||||
|
toolbarPosition: { top: 0, left: 0 },
|
||||||
|
sizes: MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
|
selectedImageNode: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
editorRoot() {
|
||||||
|
return this.$refs.editorRoot;
|
||||||
|
},
|
||||||
contentFromEditor() {
|
contentFromEditor() {
|
||||||
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
|
||||||
},
|
},
|
||||||
@@ -234,8 +267,11 @@ export default {
|
|||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
// this is considered the source of truth, we watch this property
|
// this is considered the source of truth, we watch this property
|
||||||
// on change, we toggle the signature in the editor
|
// on change, we toggle the signature in the editor
|
||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
||||||
return isEnabled && this.allowSignature && !this.isPrivate;
|
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -273,6 +309,7 @@ export default {
|
|||||||
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
||||||
this.editorView.focus();
|
this.editorView.focus();
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
this.$emit('clear-selection');
|
this.$emit('clear-selection');
|
||||||
}
|
}
|
||||||
@@ -298,7 +335,17 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.createEditorView();
|
this.createEditorView();
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
this.focusEditor(this.value);
|
this.focusEditorInputField();
|
||||||
|
|
||||||
|
// BUS Event to insert text or markdown into the editor at the
|
||||||
|
// current cursor position.
|
||||||
|
// Components using this
|
||||||
|
// 1. SearchPopover.vue
|
||||||
|
|
||||||
|
bus.$on(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
bus.$off(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reloadState(content = this.value) {
|
reloadState(content = this.value) {
|
||||||
@@ -385,6 +432,7 @@ export default {
|
|||||||
state: this.state,
|
state: this.state,
|
||||||
dispatchTransaction: tx => {
|
dispatchTransaction: tx => {
|
||||||
this.state = this.state.apply(tx);
|
this.state = this.state.apply(tx);
|
||||||
|
this.editorView.updateState(this.state);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
},
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
@@ -397,6 +445,9 @@ export default {
|
|||||||
focus: () => {
|
focus: () => {
|
||||||
this.onFocus();
|
this.onFocus();
|
||||||
},
|
},
|
||||||
|
click: () => {
|
||||||
|
this.isEditorMouseFocusedOnAnImage();
|
||||||
|
},
|
||||||
blur: () => {
|
blur: () => {
|
||||||
this.onBlur();
|
this.onBlur();
|
||||||
},
|
},
|
||||||
@@ -409,6 +460,50 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
isEditorMouseFocusedOnAnImage() {
|
||||||
|
if (!this.showImageResizeToolbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedImageNode = document.querySelector(
|
||||||
|
'img.ProseMirror-selectednode'
|
||||||
|
);
|
||||||
|
if (this.selectedImageNode) {
|
||||||
|
this.isImageNodeSelected = !!this.selectedImageNode;
|
||||||
|
// Get the position of the selected node
|
||||||
|
this.setToolbarPosition();
|
||||||
|
} else {
|
||||||
|
this.isImageNodeSelected = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setToolbarPosition() {
|
||||||
|
const editorRect = this.editorRoot.getBoundingClientRect();
|
||||||
|
const rect = this.selectedImageNode.getBoundingClientRect();
|
||||||
|
this.toolbarPosition = {
|
||||||
|
top: `${rect.top - editorRect.top - 30}px`,
|
||||||
|
left: `${rect.left - editorRect.left - 4}px`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setURLWithQueryAndImageSize(size) {
|
||||||
|
if (!this.showImageResizeToolbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setURLWithQueryAndSize(this.selectedImageNode, size, this.editorView);
|
||||||
|
this.isImageNodeSelected = false;
|
||||||
|
},
|
||||||
|
updateImgToolbarOnDelete() {
|
||||||
|
// check if the selected node is present or not on keyup
|
||||||
|
// this is needed because the user can select an image and then delete it
|
||||||
|
// in that case, the selected node will be null and we need to hide the toolbar
|
||||||
|
// otherwise, the toolbar will be visible even when the image is deleted and cause some errors
|
||||||
|
if (this.selectedImageNode) {
|
||||||
|
const hasImgSelectedNode = document.querySelector(
|
||||||
|
'img.ProseMirror-selectednode'
|
||||||
|
);
|
||||||
|
if (!hasImgSelectedNode) {
|
||||||
|
this.isImageNodeSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
isEnterToSendEnabled() {
|
isEnterToSendEnabled() {
|
||||||
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
||||||
},
|
},
|
||||||
@@ -441,11 +536,7 @@ export default {
|
|||||||
userFullName: mentionItem.name,
|
userFullName: mentionItem.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tr = this.editorView.state.tr
|
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
|
||||||
.replaceWith(this.range.from, this.range.to, node)
|
|
||||||
.insertText(` `);
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
|
||||||
this.emitOnChange();
|
|
||||||
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -455,30 +546,22 @@ export default {
|
|||||||
message: cannedItem,
|
message: cannedItem,
|
||||||
variables: this.variables,
|
variables: this.variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let from = this.range.from - 1;
|
|
||||||
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
let node = new MessageMarkdownTransformer(messageSchema).parse(
|
||||||
updatedMessage
|
updatedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
if (node.textContent === updatedMessage) {
|
const from =
|
||||||
node = this.editorView.state.schema.text(updatedMessage);
|
node.textContent === updatedMessage
|
||||||
from = this.range.from;
|
? this.range.from
|
||||||
}
|
: this.range.from - 1;
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.replaceWith(
|
this.insertNodeIntoEditor(node, from, this.range.to);
|
||||||
from,
|
|
||||||
this.range.to,
|
|
||||||
node
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
|
||||||
this.emitOnChange();
|
|
||||||
|
|
||||||
tr.scrollIntoView();
|
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -486,23 +569,14 @@ export default {
|
|||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let node = this.editorView.state.schema.text(`{{${variable}}}`);
|
|
||||||
const from = this.range.from;
|
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.replaceWith(
|
const content = `{{${variable}}}`;
|
||||||
from,
|
let node = this.editorView.state.schema.text(content);
|
||||||
this.range.to,
|
const { from, to } = this.range;
|
||||||
node
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.insertNodeIntoEditor(node, from, to);
|
||||||
this.emitOnChange();
|
|
||||||
|
|
||||||
// The `{{ }}` are added to the message, but the cursor is placed
|
|
||||||
// and onExit of suggestionsPlugin is not called. So we need to manually hide
|
|
||||||
this.showVariables = false;
|
this.showVariables = false;
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
|
||||||
tr.scrollIntoView();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
@@ -558,8 +632,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
emitOnChange() {
|
emitOnChange() {
|
||||||
this.editorView.updateState(this.state);
|
|
||||||
|
|
||||||
this.$emit('input', this.contentFromEditor);
|
this.$emit('input', this.contentFromEditor);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -602,6 +674,7 @@ export default {
|
|||||||
() => this.resetTyping(),
|
() => this.resetTyping(),
|
||||||
TYPING_INDICATOR_IDLE_TIME
|
TYPING_INDICATOR_IDLE_TIME
|
||||||
);
|
);
|
||||||
|
this.updateImgToolbarOnDelete();
|
||||||
},
|
},
|
||||||
onKeydown(event) {
|
onKeydown(event) {
|
||||||
if (this.isEnterToSendEnabled()) {
|
if (this.isEnterToSendEnabled()) {
|
||||||
@@ -619,6 +692,19 @@ export default {
|
|||||||
onFocus() {
|
onFocus() {
|
||||||
this.$emit('focus');
|
this.$emit('focus');
|
||||||
},
|
},
|
||||||
|
insertContentIntoEditor(content, defaultFrom = 0) {
|
||||||
|
const from = defaultFrom || this.editorView.state.selection.from || 0;
|
||||||
|
let node = new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||||
|
|
||||||
|
this.insertNodeIntoEditor(node, from, undefined);
|
||||||
|
},
|
||||||
|
insertNodeIntoEditor(node, from = 0, to = 0) {
|
||||||
|
this.state = insertAtCursor(this.editorView, node, from, to);
|
||||||
|
this.emitOnChange();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
scrollCursorIntoView(this.editorView);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -712,6 +798,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-warning__message {
|
.editor-warning__message {
|
||||||
@apply text-red-400 dark:text-red-400 text-sm font-normal pt-1 pb-0 px-0;
|
@apply text-red-400 dark:text-red-400 font-normal text-sm pt-1 pb-0 px-0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,10 +14,11 @@
|
|||||||
<file-upload
|
<file-upload
|
||||||
ref="upload"
|
ref="upload"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||||
|
input-id="conversationAttachment"
|
||||||
:size="4096 * 4096"
|
:size="4096 * 4096"
|
||||||
:accept="allowedFileTypes"
|
:accept="allowedFileTypes"
|
||||||
:multiple="enableMultipleFileUpload"
|
:multiple="enableMultipleFileUpload"
|
||||||
:drop="true"
|
:drop="enableDragAndDrop"
|
||||||
:drop-directory="false"
|
:drop-directory="false"
|
||||||
:data="{
|
:data="{
|
||||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||||
@@ -100,14 +101,24 @@
|
|||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<div
|
||||||
v-show="$refs.upload && $refs.upload.dropActive"
|
v-show="$refs.upload && $refs.upload.dropActive"
|
||||||
class="modal-mask"
|
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="cloud-backup" />
|
<fluent-icon icon="cloud-backup" size="40" />
|
||||||
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
<woot-button
|
||||||
|
v-if="enableInsertArticleInReply"
|
||||||
|
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||||
|
icon="document-text-link"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="smooth"
|
||||||
|
size="small"
|
||||||
|
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
|
||||||
|
@click="toggleInsertArticle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-wrap">
|
<div class="right-wrap">
|
||||||
<woot-button
|
<woot-button
|
||||||
@@ -133,6 +144,7 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|||||||
import {
|
import {
|
||||||
ALLOWED_FILE_TYPES,
|
ALLOWED_FILE_TYPES,
|
||||||
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
||||||
|
ALLOWED_FILE_TYPES_FOR_LINE,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/constants/messages';
|
||||||
import VideoCallButton from '../VideoCallButton.vue';
|
import VideoCallButton from '../VideoCallButton.vue';
|
||||||
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
||||||
@@ -228,6 +240,14 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
newConversationModalActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
portalSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@@ -251,6 +271,9 @@ export default {
|
|||||||
return this.showFileUpload || this.isNote;
|
return this.showFileUpload || this.isNote;
|
||||||
},
|
},
|
||||||
showAudioRecorderButton() {
|
showAudioRecorderButton() {
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Disable audio recorder for safari browser as recording is not supported
|
// Disable audio recorder for safari browser as recording is not supported
|
||||||
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
||||||
navigator.userAgent
|
navigator.userAgent
|
||||||
@@ -272,8 +295,14 @@ export default {
|
|||||||
if (this.isATwilioWhatsAppChannel) {
|
if (this.isATwilioWhatsAppChannel) {
|
||||||
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
|
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
|
||||||
}
|
}
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return ALLOWED_FILE_TYPES_FOR_LINE;
|
||||||
|
}
|
||||||
return ALLOWED_FILE_TYPES;
|
return ALLOWED_FILE_TYPES;
|
||||||
},
|
},
|
||||||
|
enableDragAndDrop() {
|
||||||
|
return !this.newConversationModalActive;
|
||||||
|
},
|
||||||
audioRecorderPlayStopIcon() {
|
audioRecorderPlayStopIcon() {
|
||||||
switch (this.recordingAudioState) {
|
switch (this.recordingAudioState) {
|
||||||
// playing paused recording stopped inactive destroyed
|
// playing paused recording stopped inactive destroyed
|
||||||
@@ -291,14 +320,21 @@ export default {
|
|||||||
return !this.isOnPrivateNote;
|
return !this.isOnPrivateNote;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
// channelType is sourced from inboxMixin
|
||||||
return isEnabled;
|
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||||
},
|
},
|
||||||
signatureToggleTooltip() {
|
signatureToggleTooltip() {
|
||||||
return this.sendWithSignature
|
return this.sendWithSignature
|
||||||
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
|
||||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||||
},
|
},
|
||||||
|
enableInsertArticleInReply() {
|
||||||
|
const isFeatEnabled = this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
|
||||||
|
);
|
||||||
|
return isFeatEnabled && this.portalSlug;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
ActiveStorage.start();
|
ActiveStorage.start();
|
||||||
@@ -310,13 +346,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleMessageSignature() {
|
toggleMessageSignature() {
|
||||||
this.updateUISettings({
|
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
|
||||||
send_with_signature: !this.sendWithSignature,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
replaceText(text) {
|
replaceText(text) {
|
||||||
this.$emit('replace-text', text);
|
this.$emit('replace-text', text);
|
||||||
},
|
},
|
||||||
|
toggleInsertArticle() {
|
||||||
|
this.$emit('toggle-insert-article');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -346,12 +383,4 @@ export default {
|
|||||||
@apply dark:bg-slate-800 bg-slate-100;
|
@apply dark:bg-slate-800 bg-slate-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-mask {
|
|
||||||
@apply text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
@apply text-[5rem];
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+19
-7
@@ -32,9 +32,9 @@
|
|||||||
}}</span>
|
}}</span>
|
||||||
<filter-item
|
<filter-item
|
||||||
type="sort"
|
type="sort"
|
||||||
:selected-value="chatSortFilter"
|
:selected-value="sortFilter"
|
||||||
:items="chatSortItems"
|
:items="chatSortItems"
|
||||||
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
|
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
|
||||||
@onChangeFilter="onChangeFilter"
|
@onChangeFilter="onChangeFilter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,17 +47,18 @@ import wootConstants from 'dashboard/constants/globals';
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import FilterItem from './FilterItem.vue';
|
import FilterItem from './FilterItem.vue';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilterItem,
|
FilterItem,
|
||||||
},
|
},
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, uiSettingsMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showActionsDropdown: false,
|
showActionsDropdown: false,
|
||||||
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
|
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
|
||||||
chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
|
chatSortItems: this.$t('CHAT_LIST.SORT_ORDER_ITEMS'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -69,7 +70,9 @@ export default {
|
|||||||
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
|
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
|
||||||
},
|
},
|
||||||
sortFilter() {
|
sortFilter() {
|
||||||
return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
|
return (
|
||||||
|
this.chatSortFilter || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -83,8 +86,17 @@ export default {
|
|||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
this.showActionsDropdown = false;
|
this.showActionsDropdown = false;
|
||||||
},
|
},
|
||||||
onChangeFilter(type, value) {
|
onChangeFilter(value, type) {
|
||||||
this.$emit('changeFilter', type, value);
|
this.$emit('changeFilter', value, type);
|
||||||
|
this.saveSelectedFilter(type, value);
|
||||||
|
},
|
||||||
|
saveSelectedFilter(type, value) {
|
||||||
|
this.updateUISettings({
|
||||||
|
conversations_filter_by: {
|
||||||
|
status: type === 'status' ? value : this.chatStatus,
|
||||||
|
order_by: type === 'sort' ? value : this.sortFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
:key="currentChat.id + '-' + dashboardApp.id"
|
:key="currentChat.id + '-' + dashboardApp.id"
|
||||||
:is-visible="activeIndex - 1 === index"
|
:is-visible="activeIndex - 1 === index"
|
||||||
:config="dashboardApps[index].content"
|
:config="dashboardApps[index].content"
|
||||||
|
:position="index"
|
||||||
:current-chat="currentChat"
|
:current-chat="currentChat"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
|
||||||
:class="{
|
:class="{
|
||||||
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
|
||||||
'unread-chat': hasUnread,
|
'unread-chat': hasUnread,
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
size="40px"
|
size="40px"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="py-3 px-0 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
|
class="px-0 py-3 border-b group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||||
@@ -55,44 +55,11 @@
|
|||||||
>
|
>
|
||||||
{{ currentContact.name }}
|
{{ currentContact.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p
|
<message-preview
|
||||||
v-if="lastMessageInChat"
|
v-if="lastMessageInChat"
|
||||||
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
:message="lastMessageInChat"
|
||||||
>
|
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm text-slate-700 dark:text-slate-200"
|
||||||
<fluent-icon
|
/>
|
||||||
v-if="isMessagePrivate"
|
|
||||||
size="16"
|
|
||||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
|
||||||
icon="lock-closed"
|
|
||||||
/>
|
|
||||||
<fluent-icon
|
|
||||||
v-else-if="messageByAgent"
|
|
||||||
size="16"
|
|
||||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
|
||||||
icon="arrow-reply"
|
|
||||||
/>
|
|
||||||
<fluent-icon
|
|
||||||
v-else-if="isMessageAnActivity"
|
|
||||||
size="16"
|
|
||||||
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
|
||||||
icon="info"
|
|
||||||
/>
|
|
||||||
<span v-if="lastMessageInChat.content">
|
|
||||||
{{ parsedLastMessage }}
|
|
||||||
</span>
|
|
||||||
<span v-else-if="lastMessageInChat.attachments">
|
|
||||||
<fluent-icon
|
|
||||||
v-if="attachmentIcon"
|
|
||||||
size="16"
|
|
||||||
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
|
||||||
:icon="attachmentIcon"
|
|
||||||
/>
|
|
||||||
{{ $t(`${attachmentMessageContent}`) }}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ $t('CHAT_LIST.NO_CONTENT') }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p
|
<p
|
||||||
v-else
|
v-else
|
||||||
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
@@ -106,8 +73,8 @@
|
|||||||
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="conversation--meta flex flex-col absolute right-4 top-4">
|
<div class="absolute flex flex-col conversation--meta right-4 top-4">
|
||||||
<span class="text-black-600 text-xxs font-normal leading-4 ml-auto">
|
<span class="ml-auto font-normal leading-4 text-black-600 text-xxs">
|
||||||
<time-ago
|
<time-ago
|
||||||
:last-activity-timestamp="chat.timestamp"
|
:last-activity-timestamp="chat.timestamp"
|
||||||
:created-at-timestamp="chat.created_at"
|
:created-at-timestamp="chat.created_at"
|
||||||
@@ -145,9 +112,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
|
||||||
import Thumbnail from '../Thumbnail.vue';
|
import Thumbnail from '../Thumbnail.vue';
|
||||||
|
import MessagePreview from './MessagePreview.vue';
|
||||||
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';
|
||||||
@@ -159,14 +125,6 @@ import alertMixin from 'shared/mixins/alertMixin';
|
|||||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||||
import PriorityMark from './PriorityMark.vue';
|
import PriorityMark from './PriorityMark.vue';
|
||||||
const ATTACHMENT_ICONS = {
|
|
||||||
image: 'image',
|
|
||||||
audio: 'headphones-sound-wave',
|
|
||||||
video: 'video',
|
|
||||||
file: 'document',
|
|
||||||
location: 'location',
|
|
||||||
fallback: 'link',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -175,16 +133,11 @@ export default {
|
|||||||
Thumbnail,
|
Thumbnail,
|
||||||
ConversationContextMenu,
|
ConversationContextMenu,
|
||||||
TimeAgo,
|
TimeAgo,
|
||||||
|
MessagePreview,
|
||||||
PriorityMark,
|
PriorityMark,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [
|
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||||
inboxMixin,
|
|
||||||
timeMixin,
|
|
||||||
conversationMixin,
|
|
||||||
messageFormatterMixin,
|
|
||||||
alertMixin,
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
activeLabel: {
|
activeLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -222,6 +175,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableContextMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -258,20 +215,6 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
lastMessageFileType() {
|
|
||||||
const lastMessage = this.lastMessageInChat;
|
|
||||||
const [{ file_type: fileType } = {}] = lastMessage.attachments;
|
|
||||||
return fileType;
|
|
||||||
},
|
|
||||||
|
|
||||||
attachmentIcon() {
|
|
||||||
return ATTACHMENT_ICONS[this.lastMessageFileType];
|
|
||||||
},
|
|
||||||
|
|
||||||
attachmentMessageContent() {
|
|
||||||
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
|
|
||||||
},
|
|
||||||
|
|
||||||
isActiveChat() {
|
isActiveChat() {
|
||||||
return this.currentChat.id === this.chat.id;
|
return this.currentChat.id === this.chat.id;
|
||||||
},
|
},
|
||||||
@@ -292,30 +235,6 @@ export default {
|
|||||||
return this.lastMessage(this.chat);
|
return this.lastMessage(this.chat);
|
||||||
},
|
},
|
||||||
|
|
||||||
messageByAgent() {
|
|
||||||
const lastMessage = this.lastMessageInChat;
|
|
||||||
const { message_type: messageType } = lastMessage;
|
|
||||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
|
||||||
},
|
|
||||||
|
|
||||||
isMessageAnActivity() {
|
|
||||||
const lastMessage = this.lastMessageInChat;
|
|
||||||
const { message_type: messageType } = lastMessage;
|
|
||||||
return messageType === MESSAGE_TYPE.ACTIVITY;
|
|
||||||
},
|
|
||||||
|
|
||||||
isMessagePrivate() {
|
|
||||||
const lastMessage = this.lastMessageInChat;
|
|
||||||
const { private: isPrivate } = lastMessage;
|
|
||||||
return isPrivate;
|
|
||||||
},
|
|
||||||
|
|
||||||
parsedLastMessage() {
|
|
||||||
const { content_attributes: contentAttributes } = this.lastMessageInChat;
|
|
||||||
const { email: { subject } = {} } = contentAttributes || {};
|
|
||||||
return this.getPlainText(subject || this.lastMessageInChat.content);
|
|
||||||
},
|
|
||||||
|
|
||||||
inbox() {
|
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);
|
||||||
@@ -374,6 +293,7 @@ export default {
|
|||||||
this.$emit(action, this.chat.id, this.inbox.id);
|
this.$emit(action, this.chat.id, this.inbox.id);
|
||||||
},
|
},
|
||||||
openContextMenu(e) {
|
openContextMenu(e) {
|
||||||
|
if (!this.enableContextMenu) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.$emit('context-menu-toggle', true);
|
this.$emit('context-menu-toggle', true);
|
||||||
this.contextMenu.x = e.pageX || e.clientX;
|
this.contextMenu.x = e.pageX || e.clientX;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
|
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-1 w-100 flex flex-col md:flex-row items-center justify-center"
|
class="flex-1 w-full min-w-0 flex flex-col md:flex-row items-center justify-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
|
class="flex justify-start items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0 w-[inherit]"
|
||||||
>
|
>
|
||||||
<back-button
|
<back-button
|
||||||
v-if="showBackButton"
|
v-if="showBackButton"
|
||||||
@@ -19,26 +19,31 @@
|
|||||||
:username="currentContact.name"
|
:username="currentContact.name"
|
||||||
:status="currentContact.availability_status"
|
:status="currentContact.availability_status"
|
||||||
/>
|
/>
|
||||||
<div class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0">
|
<div
|
||||||
<woot-button
|
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-[inherit] overflow-hidden"
|
||||||
variant="link"
|
>
|
||||||
color-scheme="secondary"
|
<div class="flex items-center flex-row gap-1 m-0 p-0 w-[inherit]">
|
||||||
class="overflow-hidden whitespace-nowrap text-ellipsis"
|
<woot-button
|
||||||
@click.prevent="$emit('contact-panel-toggle')"
|
variant="link"
|
||||||
>
|
color-scheme="secondary"
|
||||||
<h3
|
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
|
||||||
class="text-base inline-block leading-tight capitalize m-0 p-0 text-ellipsis overflow-hidden whitespace-nowrap text-slate-900 dark:text-slate-100"
|
@click.prevent="$emit('contact-panel-toggle')"
|
||||||
>
|
>
|
||||||
<span>{{ currentContact.name }}</span>
|
<span
|
||||||
<fluent-icon
|
class="text-base leading-tight text-slate-900 dark:text-slate-100"
|
||||||
v-if="!isHMACVerified"
|
>
|
||||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
{{ currentContact.name }}
|
||||||
size="14"
|
</span>
|
||||||
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0.5"
|
</woot-button>
|
||||||
icon="warning"
|
<fluent-icon
|
||||||
/>
|
v-if="!isHMACVerified"
|
||||||
</h3>
|
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||||
</woot-button>
|
size="14"
|
||||||
|
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0 min-w-[14px]"
|
||||||
|
icon="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
|
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
|
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
|
||||||
<div :class="wrapClass">
|
<div :class="wrapClass">
|
||||||
<div v-if="isFailed" class="message-failed--alert">
|
<div v-if="isFailed && !hasOneDayPassed" class="message-failed--alert">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@@ -22,18 +22,13 @@
|
|||||||
:bcc="emailHeadAttributes.bcc"
|
:bcc="emailHeadAttributes.bcc"
|
||||||
:is-incoming="isIncoming"
|
:is-incoming="isIncoming"
|
||||||
/>
|
/>
|
||||||
<blockquote v-if="storyReply" class="story-reply-quote">
|
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
|
||||||
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
<bubble-reply-to
|
||||||
<bubble-image
|
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
|
||||||
v-if="!hasImgStoryError && storyUrl"
|
:message="inReplyTo"
|
||||||
:url="storyUrl"
|
:message-type="data.message_type"
|
||||||
@error="onStoryLoadError"
|
:parent-has-attachments="hasAttachments"
|
||||||
/>
|
/>
|
||||||
<bubble-video
|
|
||||||
v-else-if="hasImgStoryError && storyUrl"
|
|
||||||
:url="storyUrl"
|
|
||||||
/>
|
|
||||||
</blockquote>
|
|
||||||
<bubble-text
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
@@ -53,10 +48,15 @@
|
|||||||
</span>
|
</span>
|
||||||
<div v-if="!isPending && hasAttachments">
|
<div v-if="!isPending && hasAttachments">
|
||||||
<div v-for="attachment in attachments" :key="attachment.id">
|
<div v-for="attachment in attachments" :key="attachment.id">
|
||||||
|
<instagram-story
|
||||||
|
v-if="isAnInstagramStory"
|
||||||
|
:story-url="attachment.data_url"
|
||||||
|
@error="onMediaLoadError"
|
||||||
|
/>
|
||||||
<bubble-image-audio-video
|
<bubble-image-audio-video
|
||||||
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||||
:attachment="attachment"
|
:attachment="attachment"
|
||||||
@error="onImageLoadError"
|
@error="onMediaLoadError"
|
||||||
/>
|
/>
|
||||||
<bubble-location
|
<bubble-location
|
||||||
v-else-if="attachment.file_type === 'location'"
|
v-else-if="attachment.file_type === 'location'"
|
||||||
@@ -69,9 +69,6 @@
|
|||||||
:name="data.content"
|
:name="data.content"
|
||||||
:phone-number="attachment.fallback_title"
|
:phone-number="attachment.fallback_title"
|
||||||
/>
|
/>
|
||||||
<instagram-image-error-placeholder
|
|
||||||
v-else-if="hasImageError && hasInstagramStory"
|
|
||||||
/>
|
|
||||||
<bubble-file v-else :url="attachment.data_url" />
|
<bubble-file v-else :url="attachment.data_url" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +80,6 @@
|
|||||||
:story-id="`${storyId}`"
|
:story-id="`${storyId}`"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
|
||||||
:is-email="isEmailContentType"
|
:is-email="isEmailContentType"
|
||||||
:is-private="data.private"
|
:is-private="data.private"
|
||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
@@ -124,6 +120,7 @@
|
|||||||
:message="data"
|
:message="data"
|
||||||
@open="openContextMenu"
|
@open="openContextMenu"
|
||||||
@close="closeContextMenu"
|
@close="closeContextMenu"
|
||||||
|
@replyTo="handleReplyTo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -131,40 +128,43 @@
|
|||||||
<script>
|
<script>
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import BubbleActions from './bubble/Actions.vue';
|
import BubbleActions from './bubble/Actions.vue';
|
||||||
|
import BubbleContact from './bubble/Contact.vue';
|
||||||
import BubbleFile from './bubble/File.vue';
|
import BubbleFile from './bubble/File.vue';
|
||||||
import BubbleImage from './bubble/Image.vue';
|
|
||||||
import BubbleVideo from './bubble/Video.vue';
|
|
||||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
|
||||||
import BubbleIntegration from './bubble/Integration.vue';
|
import BubbleIntegration from './bubble/Integration.vue';
|
||||||
import BubbleLocation from './bubble/Location.vue';
|
import BubbleLocation from './bubble/Location.vue';
|
||||||
import BubbleMailHead from './bubble/MailHead.vue';
|
import BubbleMailHead from './bubble/MailHead.vue';
|
||||||
|
import BubbleReplyTo from './bubble/ReplyTo.vue';
|
||||||
import BubbleText from './bubble/Text.vue';
|
import BubbleText from './bubble/Text.vue';
|
||||||
import BubbleContact from './bubble/Contact.vue';
|
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
|
||||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||||
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
|
import InstagramStory from './bubble/InstagramStory.vue';
|
||||||
|
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BubbleActions,
|
BubbleActions,
|
||||||
|
BubbleContact,
|
||||||
BubbleFile,
|
BubbleFile,
|
||||||
BubbleImage,
|
|
||||||
BubbleVideo,
|
|
||||||
BubbleImageAudioVideo,
|
BubbleImageAudioVideo,
|
||||||
BubbleIntegration,
|
BubbleIntegration,
|
||||||
BubbleLocation,
|
BubbleLocation,
|
||||||
BubbleMailHead,
|
BubbleMailHead,
|
||||||
|
BubbleReplyTo,
|
||||||
BubbleText,
|
BubbleText,
|
||||||
BubbleContact,
|
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
InstagramStory,
|
||||||
|
InstagramStoryReply,
|
||||||
Spinner,
|
Spinner,
|
||||||
instagramImageErrorPlaceholder,
|
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -180,22 +180,25 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
hasInstagramStory: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isWebWidgetInbox: {
|
isWebWidgetInbox: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
inboxSupportsReplyTo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
inReplyTo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showContextMenu: false,
|
showContextMenu: false,
|
||||||
hasImageError: false,
|
hasMediaLoadError: false,
|
||||||
contextMenuPosition: {},
|
contextMenuPosition: {},
|
||||||
showBackgroundHighlight: false,
|
showBackgroundHighlight: false,
|
||||||
hasImgStoryError: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -207,6 +210,10 @@ export default {
|
|||||||
created_at: this.data.created_at || '',
|
created_at: this.data.created_at || '',
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
hasOneDayPassed() {
|
||||||
|
// Disable retry button if the message is failed and the message is older than 24 hours
|
||||||
|
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
|
||||||
|
},
|
||||||
shouldRenderMessage() {
|
shouldRenderMessage() {
|
||||||
return (
|
return (
|
||||||
this.hasAttachments ||
|
this.hasAttachments ||
|
||||||
@@ -264,18 +271,29 @@ export default {
|
|||||||
) + botMessageContent
|
) + botMessageContent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
inReplyToMessageId() {
|
||||||
|
// Why not use the inReplyTo object directly?
|
||||||
|
// Glad you asked! The inReplyTo object may or may not be available
|
||||||
|
// depending on the current scroll position of the message list
|
||||||
|
// since old messages are only loaded when the user scrolls up
|
||||||
|
return this.data.content_attributes?.in_reply_to;
|
||||||
|
},
|
||||||
|
isAnInstagramStory() {
|
||||||
|
return this.contentAttributes.image_type === 'story_mention';
|
||||||
|
},
|
||||||
contextMenuEnabledOptions() {
|
contextMenuEnabledOptions() {
|
||||||
return {
|
return {
|
||||||
copy: this.hasText,
|
copy: this.hasText,
|
||||||
delete: this.hasText || this.hasAttachments,
|
delete: this.hasText || this.hasAttachments,
|
||||||
cannedResponse: this.isOutgoing && this.hasText,
|
cannedResponse: this.isOutgoing && this.hasText,
|
||||||
|
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contentAttributes() {
|
contentAttributes() {
|
||||||
return this.data.content_attributes || {};
|
return this.data.content_attributes || {};
|
||||||
},
|
},
|
||||||
externalError() {
|
externalError() {
|
||||||
return this.contentAttributes.external_error || null;
|
return this.contentAttributes.external_error || '';
|
||||||
},
|
},
|
||||||
sender() {
|
sender() {
|
||||||
return this.data.sender || {};
|
return this.data.sender || {};
|
||||||
@@ -292,9 +310,6 @@ export default {
|
|||||||
storyUrl() {
|
storyUrl() {
|
||||||
return this.contentAttributes.story_url || null;
|
return this.contentAttributes.story_url || null;
|
||||||
},
|
},
|
||||||
storyReply() {
|
|
||||||
return this.storyUrl && this.hasInstagramStory;
|
|
||||||
},
|
|
||||||
contentType() {
|
contentType() {
|
||||||
const {
|
const {
|
||||||
data: { content_type: contentType },
|
data: { content_type: contentType },
|
||||||
@@ -318,6 +333,8 @@ export default {
|
|||||||
left: isLeftAligned,
|
left: isLeftAligned,
|
||||||
right: isRightAligned,
|
right: isRightAligned,
|
||||||
'has-context-menu': this.showContextMenu,
|
'has-context-menu': this.showContextMenu,
|
||||||
|
// this handles the offset required to align the context menu button
|
||||||
|
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||||
'has-tweet-menu': this.isATweet,
|
'has-tweet-menu': this.isATweet,
|
||||||
'has-bg': this.showBackgroundHighlight,
|
'has-bg': this.showBackgroundHighlight,
|
||||||
};
|
};
|
||||||
@@ -373,7 +390,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.isFailed) {
|
if (this.isFailed) {
|
||||||
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`);
|
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -431,13 +448,11 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
data() {
|
data() {
|
||||||
this.hasImageError = false;
|
this.hasMediaLoadError = false;
|
||||||
this.hasImgStoryError = false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.hasImageError = false;
|
this.hasMediaLoadError = false;
|
||||||
this.hasImgStoryError = false;
|
|
||||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||||
this.setupHighlightTimer();
|
this.setupHighlightTimer();
|
||||||
},
|
},
|
||||||
@@ -447,13 +462,13 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isAttachmentImageVideoAudio(fileType) {
|
isAttachmentImageVideoAudio(fileType) {
|
||||||
return ['image', 'audio', 'video'].includes(fileType);
|
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
|
||||||
},
|
},
|
||||||
hasMediaAttachment(type) {
|
hasMediaAttachment(type) {
|
||||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||||
const { attachments = [{}] } = this.data;
|
const { attachments = [{}] } = this.data;
|
||||||
const { file_type: fileType } = attachments[0];
|
const { file_type: fileType } = attachments[0];
|
||||||
return fileType === type && !this.hasImageError;
|
return fileType === type && !this.hasMediaLoadError;
|
||||||
}
|
}
|
||||||
if (this.storyReply) {
|
if (this.storyReply) {
|
||||||
return true;
|
return true;
|
||||||
@@ -466,11 +481,8 @@ export default {
|
|||||||
async retrySendMessage() {
|
async retrySendMessage() {
|
||||||
await this.$store.dispatch('sendMessageWithData', this.data);
|
await this.$store.dispatch('sendMessageWithData', this.data);
|
||||||
},
|
},
|
||||||
onImageLoadError() {
|
onMediaLoadError() {
|
||||||
this.hasImageError = true;
|
this.hasMediaLoadError = true;
|
||||||
},
|
|
||||||
onStoryLoadError() {
|
|
||||||
this.hasImgStoryError = true;
|
|
||||||
},
|
},
|
||||||
openContextMenu(e) {
|
openContextMenu(e) {
|
||||||
const shouldSkipContextMenu =
|
const shouldSkipContextMenu =
|
||||||
@@ -494,6 +506,13 @@ export default {
|
|||||||
this.showContextMenu = false;
|
this.showContextMenu = false;
|
||||||
this.contextMenuPosition = { x: null, y: null };
|
this.contextMenuPosition = { x: null, y: null };
|
||||||
},
|
},
|
||||||
|
handleReplyTo() {
|
||||||
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||||
|
const { conversation_id: conversationId, id: replyTo } = this.data;
|
||||||
|
|
||||||
|
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
|
||||||
|
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
||||||
|
},
|
||||||
setupHighlightTimer() {
|
setupHighlightTimer() {
|
||||||
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
||||||
return;
|
return;
|
||||||
@@ -613,6 +632,8 @@ li.right {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.left.has-tweet-menu .context-menu {
|
li.left.has-tweet-menu .context-menu {
|
||||||
|
// this handles the offset required to align the context menu button
|
||||||
|
// extra alignment is required since a tweet message has a the user name and avatar below it
|
||||||
@apply mb-6;
|
@apply mb-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,8 +714,4 @@ li.right {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-reply-quote {
|
|
||||||
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
<template v-if="showMessageType">
|
||||||
|
<fluent-icon
|
||||||
|
v-if="isMessagePrivate"
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||||
|
icon="lock-closed"
|
||||||
|
/>
|
||||||
|
<fluent-icon
|
||||||
|
v-else-if="messageByAgent"
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||||
|
icon="arrow-reply"
|
||||||
|
/>
|
||||||
|
<fluent-icon
|
||||||
|
v-else-if="isMessageAnActivity"
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
|
||||||
|
icon="info"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-if="message.content && isMessageSticker">
|
||||||
|
<fluent-icon
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||||
|
icon="image"
|
||||||
|
/>
|
||||||
|
{{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="message.content">
|
||||||
|
{{ parsedLastMessage }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="message.attachments">
|
||||||
|
<fluent-icon
|
||||||
|
v-if="attachmentIcon && showMessageType"
|
||||||
|
size="16"
|
||||||
|
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
|
||||||
|
:icon="attachmentIcon"
|
||||||
|
/>
|
||||||
|
{{ $t(`${attachmentMessageContent}`) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ defaultEmptyMessage || $t('CHAT_LIST.NO_CONTENT') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MessagePreview',
|
||||||
|
mixins: [messageFormatterMixin],
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showMessageType: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
defaultEmptyMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
messageByAgent() {
|
||||||
|
const { message_type: messageType } = this.message;
|
||||||
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
},
|
||||||
|
isMessageAnActivity() {
|
||||||
|
const { message_type: messageType } = this.message;
|
||||||
|
return messageType === MESSAGE_TYPE.ACTIVITY;
|
||||||
|
},
|
||||||
|
isMessagePrivate() {
|
||||||
|
const { private: isPrivate } = this.message;
|
||||||
|
return isPrivate;
|
||||||
|
},
|
||||||
|
parsedLastMessage() {
|
||||||
|
const { content_attributes: contentAttributes } = this.message;
|
||||||
|
const { email: { subject } = {} } = contentAttributes || {};
|
||||||
|
return this.getPlainText(subject || this.message.content);
|
||||||
|
},
|
||||||
|
lastMessageFileType() {
|
||||||
|
const [{ file_type: fileType } = {}] = this.message.attachments;
|
||||||
|
return fileType;
|
||||||
|
},
|
||||||
|
attachmentIcon() {
|
||||||
|
return ATTACHMENT_ICONS[this.lastMessageFileType];
|
||||||
|
},
|
||||||
|
attachmentMessageContent() {
|
||||||
|
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
|
||||||
|
},
|
||||||
|
isMessageSticker() {
|
||||||
|
return this.message && this.message.content_type === 'sticker';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-0 flex flex-col justify-between h-full flex-grow min-w-0">
|
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
|
||||||
<banner
|
<banner
|
||||||
v-if="!currentChat.can_reply"
|
v-if="!currentChat.can_reply"
|
||||||
color-scheme="alert"
|
color-scheme="alert"
|
||||||
@@ -7,21 +7,12 @@
|
|||||||
:href-link="replyWindowLink"
|
:href-link="replyWindowLink"
|
||||||
:href-link-text="replyWindowLinkText"
|
:href-link-text="replyWindowLinkText"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<banner
|
|
||||||
v-if="isATweet"
|
|
||||||
color-scheme="gray"
|
|
||||||
:banner-message="tweetBannerText"
|
|
||||||
:has-close-button="hasSelectedTweetId"
|
|
||||||
@close="removeTweetSelection"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
||||||
:icon="isRightOrLeftIcon"
|
:icon="isRightOrLeftIcon"
|
||||||
@click="onToggleContactPanel"
|
@click="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
@@ -40,8 +31,9 @@
|
|||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
/>
|
/>
|
||||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||||
<span>
|
<span>
|
||||||
@@ -61,8 +53,9 @@
|
|||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
|
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||||
|
:in-reply-to="getInReplyToMessage(message)"
|
||||||
/>
|
/>
|
||||||
<conversation-label-suggestion
|
<conversation-label-suggestion
|
||||||
v-if="shouldShowLabelSuggestions"
|
v-if="shouldShowLabelSuggestions"
|
||||||
@@ -87,8 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<reply-box
|
<reply-box
|
||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:is-a-tweet="isATweet"
|
|
||||||
:selected-tweet="selectedTweet"
|
|
||||||
:popout-reply-box.sync="isPopoutReplyBox"
|
:popout-reply-box.sync="isPopoutReplyBox"
|
||||||
@click="showPopoutReplyBox"
|
@click="showPopoutReplyBox"
|
||||||
/>
|
/>
|
||||||
@@ -110,7 +101,7 @@ import { mapGetters } from 'vuex';
|
|||||||
import conversationMixin, {
|
import conversationMixin, {
|
||||||
filterDuplicateSourceMessages,
|
filterDuplicateSourceMessages,
|
||||||
} from '../../../mixins/conversations';
|
} from '../../../mixins/conversations';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import configMixin from 'shared/mixins/configMixin';
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
@@ -153,7 +144,6 @@ export default {
|
|||||||
isLoadingPrevious: true,
|
isLoadingPrevious: true,
|
||||||
heightBeforeLoad: null,
|
heightBeforeLoad: null,
|
||||||
conversationPanel: null,
|
conversationPanel: null,
|
||||||
selectedTweetId: null,
|
|
||||||
hasUserScrolled: false,
|
hasUserScrolled: false,
|
||||||
isProgrammaticScroll: false,
|
isProgrammaticScroll: false,
|
||||||
isPopoutReplyBox: false,
|
isPopoutReplyBox: false,
|
||||||
@@ -164,12 +154,14 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
allConversations: 'getAllConversations',
|
allConversations: 'getAllConversations',
|
||||||
inboxesList: 'inboxes/getInboxes',
|
inboxesList: 'inboxes/getInboxes',
|
||||||
listLoadingStatus: 'getAllMessagesLoaded',
|
listLoadingStatus: 'getAllMessagesLoaded',
|
||||||
loadingChatList: 'getChatListLoadingStatus',
|
loadingChatList: 'getChatListLoadingStatus',
|
||||||
appIntegrations: 'integrations/getAppIntegrations',
|
appIntegrations: 'integrations/getAppIntegrations',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
isOpen() {
|
isOpen() {
|
||||||
@@ -189,16 +181,6 @@ export default {
|
|||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||||
},
|
},
|
||||||
hasSelectedTweetId() {
|
|
||||||
return !!this.selectedTweetId;
|
|
||||||
},
|
|
||||||
tweetBannerText() {
|
|
||||||
return !this.selectedTweetId
|
|
||||||
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
|
|
||||||
: `
|
|
||||||
${this.$t('CONVERSATION.REPLYING_TO')}
|
|
||||||
${this.selectedTweet.content}` || '';
|
|
||||||
},
|
|
||||||
typingUsersList() {
|
typingUsersList() {
|
||||||
const userList = this.$store.getters[
|
const userList = this.$store.getters[
|
||||||
'conversationTypingStatus/getUserList'
|
'conversationTypingStatus/getUserList'
|
||||||
@@ -253,21 +235,6 @@ export default {
|
|||||||
isATweet() {
|
isATweet() {
|
||||||
return this.conversationType === 'tweet';
|
return this.conversationType === 'tweet';
|
||||||
},
|
},
|
||||||
|
|
||||||
hasInstagramStory() {
|
|
||||||
return this.conversationType === 'instagram_direct_message';
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedTweet() {
|
|
||||||
if (this.selectedTweetId) {
|
|
||||||
const { messages = [] } = this.currentChat;
|
|
||||||
const [selectedMessage] = messages.filter(
|
|
||||||
message => message.id === this.selectedTweetId
|
|
||||||
);
|
|
||||||
return selectedMessage || {};
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
isRightOrLeftIcon() {
|
isRightOrLeftIcon() {
|
||||||
if (this.isContactPanelOpen) {
|
if (this.isContactPanelOpen) {
|
||||||
return 'arrow-chevron-right';
|
return 'arrow-chevron-right';
|
||||||
@@ -316,6 +283,14 @@ export default {
|
|||||||
unreadMessageCount() {
|
unreadMessageCount() {
|
||||||
return this.currentChat.unread_count || 0;
|
return this.currentChat.unread_count || 0;
|
||||||
},
|
},
|
||||||
|
inboxSupportsReplyTo() {
|
||||||
|
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
|
||||||
|
const outgoing =
|
||||||
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING) &&
|
||||||
|
!this.is360DialogWhatsAppChannel;
|
||||||
|
|
||||||
|
return { incoming, outgoing };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -326,7 +301,6 @@ export default {
|
|||||||
this.fetchAllAttachmentsFromCurrentChat();
|
this.fetchAllAttachmentsFromCurrentChat();
|
||||||
this.fetchSuggestions();
|
this.fetchSuggestions();
|
||||||
this.messageSentSinceOpened = false;
|
this.messageSentSinceOpened = false;
|
||||||
this.selectedTweetId = null;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -334,7 +308,6 @@ export default {
|
|||||||
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||||
// when a new message comes in, we refetch the label suggestions
|
// when a new message comes in, we refetch the label suggestions
|
||||||
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
||||||
bus.$on(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
|
||||||
// when a message is sent we set the flag to true this hides the label suggestions,
|
// when a message is sent we set the flag to true this hides the label suggestions,
|
||||||
// until the chat is changed and the flag is reset in the watch for currentChat
|
// until the chat is changed and the flag is reset in the watch for currentChat
|
||||||
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
|
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
|
||||||
@@ -369,7 +342,7 @@ export default {
|
|||||||
// method available in mixin, need to ensure that integrations are present
|
// method available in mixin, need to ensure that integrations are present
|
||||||
await this.fetchIntegrationsIfRequired();
|
await this.fetchIntegrationsIfRequired();
|
||||||
|
|
||||||
if (!this.isAIIntegrationEnabled) {
|
if (!this.isLabelSuggestionFeatureEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,10 +378,6 @@ export default {
|
|||||||
},
|
},
|
||||||
removeBusListeners() {
|
removeBusListeners() {
|
||||||
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||||
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
|
||||||
},
|
|
||||||
setSelectedTweet(tweetId) {
|
|
||||||
this.selectedTweetId = tweetId;
|
|
||||||
},
|
},
|
||||||
onScrollToMessage({ messageId = '' } = {}) {
|
onScrollToMessage({ messageId = '' } = {}) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -531,8 +500,18 @@ export default {
|
|||||||
makeMessagesRead() {
|
makeMessagesRead() {
|
||||||
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
||||||
},
|
},
|
||||||
removeTweetSelection() {
|
|
||||||
this.selectedTweetId = null;
|
getInReplyToMessage(parentMessage) {
|
||||||
|
if (!parentMessage) return {};
|
||||||
|
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
|
||||||
|
if (!inReplyToMessageId) return {};
|
||||||
|
|
||||||
|
return this.currentChat?.messages.find(message => {
|
||||||
|
if (message.id === inReplyToMessageId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,18 @@
|
|||||||
:popout-reply-box="popoutReplyBox"
|
:popout-reply-box="popoutReplyBox"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
/>
|
/>
|
||||||
|
<article-search-popover
|
||||||
|
v-if="showArticleSearchPopover && connectedPortalSlug"
|
||||||
|
:selected-portal-slug="connectedPortalSlug"
|
||||||
|
@insert="handleInsert"
|
||||||
|
@close="onSearchPopoverClose"
|
||||||
|
/>
|
||||||
<div class="reply-box__top">
|
<div class="reply-box__top">
|
||||||
|
<reply-to-message
|
||||||
|
v-if="shouldShowReplyToMessage"
|
||||||
|
:message="inReplyTo"
|
||||||
|
@dismiss="resetReplyToMessage"
|
||||||
|
/>
|
||||||
<canned-response
|
<canned-response
|
||||||
v-if="showMentions && hasSlashCommand"
|
v-if="showMentions && hasSlashCommand"
|
||||||
v-on-clickaway="hideMentions"
|
v-on-clickaway="hideMentions"
|
||||||
@@ -30,7 +41,7 @@
|
|||||||
v-if="showEmojiPicker"
|
v-if="showEmojiPicker"
|
||||||
v-on-clickaway="hideEmojiPicker"
|
v-on-clickaway="hideEmojiPicker"
|
||||||
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
|
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
|
||||||
:on-click="emojiOnClick"
|
:on-click="addIntoEditor"
|
||||||
/>
|
/>
|
||||||
<reply-email-head
|
<reply-email-head
|
||||||
v-if="showReplyHead"
|
v-if="showReplyHead"
|
||||||
@@ -74,6 +85,7 @@
|
|||||||
:variables="messageVariables"
|
:variables="messageVariables"
|
||||||
:signature="signatureToApply"
|
:signature="signatureToApply"
|
||||||
:allow-signature="true"
|
:allow-signature="true"
|
||||||
|
:channel-type="channelType"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@@ -86,6 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
||||||
<attachment-preview
|
<attachment-preview
|
||||||
|
class="flex-col mt-4"
|
||||||
:attachments="attachedFiles"
|
:attachments="attachedFiles"
|
||||||
:remove-attachment="removeAttachment"
|
:remove-attachment="removeAttachment"
|
||||||
/>
|
/>
|
||||||
@@ -115,9 +128,12 @@
|
|||||||
:toggle-audio-recorder="toggleAudioRecorder"
|
:toggle-audio-recorder="toggleAudioRecorder"
|
||||||
:toggle-emoji-picker="toggleEmojiPicker"
|
:toggle-emoji-picker="toggleEmojiPicker"
|
||||||
:message="message"
|
:message="message"
|
||||||
|
:portal-slug="connectedPortalSlug"
|
||||||
|
:new-conversation-modal-active="newConversationModalActive"
|
||||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||||
@toggle-editor="toggleRichContentEditor"
|
@toggle-editor="toggleRichContentEditor"
|
||||||
@replace-text="replaceText"
|
@replace-text="replaceText"
|
||||||
|
@toggle-insert-article="toggleInsertArticle"
|
||||||
/>
|
/>
|
||||||
<whatsapp-templates
|
<whatsapp-templates
|
||||||
:inbox-id="inbox.id"
|
:inbox-id="inbox.id"
|
||||||
@@ -141,11 +157,13 @@ import { mixin as clickaway } from 'vue-clickaway';
|
|||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
import CannedResponse from './CannedResponse.vue';
|
||||||
|
import ReplyToMessage from './ReplyToMessage.vue';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
||||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
|
||||||
|
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
@@ -162,7 +180,7 @@ import {
|
|||||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import { trimContent, debounce } from '@chatwoot/utils';
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
@@ -177,12 +195,16 @@ import {
|
|||||||
extractTextFromMarkdown,
|
extractTextFromMarkdown,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
|
||||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
CannedResponse,
|
CannedResponse,
|
||||||
|
ReplyToMessage,
|
||||||
ResizableTextArea,
|
ResizableTextArea,
|
||||||
AttachmentPreview,
|
AttachmentPreview,
|
||||||
ReplyTopPanel,
|
ReplyTopPanel,
|
||||||
@@ -193,6 +215,7 @@ export default {
|
|||||||
Banner,
|
Banner,
|
||||||
WhatsappTemplates,
|
WhatsappTemplates,
|
||||||
MessageSignatureMissingAlert,
|
MessageSignatureMissingAlert,
|
||||||
|
ArticleSearchPopover,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
clickaway,
|
clickaway,
|
||||||
@@ -204,14 +227,6 @@ export default {
|
|||||||
fileUploadMixin,
|
fileUploadMixin,
|
||||||
],
|
],
|
||||||
props: {
|
props: {
|
||||||
selectedTweet: {
|
|
||||||
type: [Object, String],
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
isATweet: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
popoutReplyBox: {
|
popoutReplyBox: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -220,6 +235,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
message: '',
|
message: '',
|
||||||
|
inReplyTo: {},
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
@@ -241,6 +257,8 @@ export default {
|
|||||||
showUserMentions: false,
|
showUserMentions: false,
|
||||||
showCannedMenu: false,
|
showCannedMenu: false,
|
||||||
showVariablesMenu: false,
|
showVariablesMenu: false,
|
||||||
|
newConversationModalActive: false,
|
||||||
|
showArticleSearchPopover: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -251,7 +269,16 @@ export default {
|
|||||||
lastEmail: 'getLastEmailInSelectedChat',
|
lastEmail: 'getLastEmailInSelectedChat',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
}),
|
}),
|
||||||
|
shouldShowReplyToMessage() {
|
||||||
|
return (
|
||||||
|
this.inReplyTo?.id &&
|
||||||
|
!this.isPrivate &&
|
||||||
|
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
|
||||||
|
!this.is360DialogWhatsAppChannel
|
||||||
|
);
|
||||||
|
},
|
||||||
showRichContentEditor() {
|
showRichContentEditor() {
|
||||||
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
||||||
return true;
|
return true;
|
||||||
@@ -323,10 +350,7 @@ export default {
|
|||||||
return this.maxLength - this.message.length;
|
return this.maxLength - this.message.length;
|
||||||
},
|
},
|
||||||
isReplyButtonDisabled() {
|
isReplyButtonDisabled() {
|
||||||
if (this.isATweet && !this.inReplyTo && !this.isOnPrivateNote) {
|
if (this.isATwitterInbox) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -359,11 +383,6 @@ export default {
|
|||||||
if (this.isASmsInbox) {
|
if (this.isASmsInbox) {
|
||||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||||
}
|
}
|
||||||
if (this.isATwitterInbox) {
|
|
||||||
if (this.conversationType === 'tweet') {
|
|
||||||
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||||
},
|
},
|
||||||
showFileUpload() {
|
showFileUpload() {
|
||||||
@@ -374,15 +393,14 @@ export default {
|
|||||||
this.isAPIInbox ||
|
this.isAPIInbox ||
|
||||||
this.isAnEmailChannel ||
|
this.isAnEmailChannel ||
|
||||||
this.isASmsInbox ||
|
this.isASmsInbox ||
|
||||||
this.isATelegramChannel
|
this.isATelegramChannel ||
|
||||||
|
this.isALineChannel
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
replyButtonLabel() {
|
replyButtonLabel() {
|
||||||
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
|
||||||
if (this.isPrivate) {
|
if (this.isPrivate) {
|
||||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||||
} else if (this.conversationType === 'tweet') {
|
|
||||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.TWEET');
|
|
||||||
}
|
}
|
||||||
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
||||||
? '(⌘ + ↵)'
|
? '(⌘ + ↵)'
|
||||||
@@ -416,10 +434,6 @@ export default {
|
|||||||
isOnPrivateNote() {
|
isOnPrivateNote() {
|
||||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||||
},
|
},
|
||||||
inReplyTo() {
|
|
||||||
const selectedTweet = this.selectedTweet || {};
|
|
||||||
return selectedTweet.id;
|
|
||||||
},
|
|
||||||
isOnExpandedLayout() {
|
isOnExpandedLayout() {
|
||||||
const {
|
const {
|
||||||
LAYOUT_TYPES: { CONDENSED },
|
LAYOUT_TYPES: { CONDENSED },
|
||||||
@@ -437,15 +451,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
replyToUserLength() {
|
|
||||||
const selectedTweet = this.selectedTweet || {};
|
|
||||||
const {
|
|
||||||
sender: {
|
|
||||||
additional_attributes: { screen_name: screenName = '' } = {},
|
|
||||||
} = {},
|
|
||||||
} = selectedTweet;
|
|
||||||
return screenName ? screenName.length : 0;
|
|
||||||
},
|
|
||||||
isMessageEmpty() {
|
isMessageEmpty() {
|
||||||
if (!this.message) {
|
if (!this.message) {
|
||||||
return true;
|
return true;
|
||||||
@@ -470,8 +475,7 @@ export default {
|
|||||||
return !!this.signatureToApply;
|
return !!this.signatureToApply;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||||
return isEnabled;
|
|
||||||
},
|
},
|
||||||
editorMessageKey() {
|
editorMessageKey() {
|
||||||
const { editor_message_key: isEnabled } = this.uiSettings;
|
const { editor_message_key: isEnabled } = this.uiSettings;
|
||||||
@@ -493,7 +497,11 @@ export default {
|
|||||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
},
|
},
|
||||||
audioRecordFormat() {
|
audioRecordFormat() {
|
||||||
if (this.isAWhatsAppChannel || this.isAPIInbox) {
|
if (
|
||||||
|
this.isAWhatsAppChannel ||
|
||||||
|
this.isAPIInbox ||
|
||||||
|
this.isATelegramChannel
|
||||||
|
) {
|
||||||
return AUDIO_FORMATS.OGG;
|
return AUDIO_FORMATS.OGG;
|
||||||
}
|
}
|
||||||
return AUDIO_FORMATS.WAV;
|
return AUDIO_FORMATS.WAV;
|
||||||
@@ -510,11 +518,18 @@ export default {
|
|||||||
? this.messageSignature
|
? this.messageSignature
|
||||||
: extractTextFromMarkdown(this.messageSignature);
|
: extractTextFromMarkdown(this.messageSignature);
|
||||||
},
|
},
|
||||||
|
connectedPortalSlug() {
|
||||||
|
const { help_center: portal = {} } = this.inbox;
|
||||||
|
const { slug = '' } = portal;
|
||||||
|
return slug;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
const { can_reply: canReply } = conversation;
|
const { can_reply: canReply } = conversation;
|
||||||
|
|
||||||
|
this.setCCAndToEmailsFromLastChat();
|
||||||
|
|
||||||
if (this.isOnPrivateNote) {
|
if (this.isOnPrivateNote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -525,7 +540,7 @@ export default {
|
|||||||
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCCAndToEmailsFromLastChat();
|
this.fetchAndSetReplyTo();
|
||||||
},
|
},
|
||||||
conversationIdByRoute(conversationId, oldConversationId) {
|
conversationIdByRoute(conversationId, oldConversationId) {
|
||||||
if (conversationId !== oldConversationId) {
|
if (conversationId !== oldConversationId) {
|
||||||
@@ -534,17 +549,25 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
message(updatedMessage) {
|
message(updatedMessage) {
|
||||||
this.hasSlashCommand =
|
// Check if the message starts with a slash.
|
||||||
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
const bodyWithoutSignature = removeSignature(
|
||||||
const hasNextWord = updatedMessage.includes(' ');
|
updatedMessage,
|
||||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
this.signatureToApply
|
||||||
if (isShortCodeActive) {
|
);
|
||||||
this.mentionSearchKey = updatedMessage.substring(1);
|
const startsWithSlash = bodyWithoutSignature.startsWith('/');
|
||||||
this.showMentions = true;
|
|
||||||
} else {
|
// Determine if the user is potentially typing a slash command.
|
||||||
this.mentionSearchKey = '';
|
// This is true if the message starts with a slash and the rich content editor is not active.
|
||||||
this.showMentions = false;
|
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
|
||||||
}
|
this.showMentions = this.hasSlashCommand;
|
||||||
|
|
||||||
|
// If a slash command is active, extract the command text after the slash.
|
||||||
|
// If not, reset the mentionSearchKey.
|
||||||
|
this.mentionSearchKey = this.hasSlashCommand
|
||||||
|
? bodyWithoutSignature.substring(1)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Autosave the current message draft.
|
||||||
this.doAutoSaveDraft();
|
this.doAutoSaveDraft();
|
||||||
},
|
},
|
||||||
replyType(updatedReplyType, oldReplyType) {
|
replyType(updatedReplyType, oldReplyType) {
|
||||||
@@ -567,12 +590,49 @@ export default {
|
|||||||
500,
|
500,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.fetchAndSetReplyTo();
|
||||||
|
bus.$on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||||
|
|
||||||
|
// A hacky fix to solve the drag and drop
|
||||||
|
// Is showing on top of new conversation modal drag and drop
|
||||||
|
// TODO need to find a better solution
|
||||||
|
bus.$on(
|
||||||
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||||
|
this.onNewConversationModalActive
|
||||||
|
);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
document.removeEventListener('paste', this.onPaste);
|
document.removeEventListener('paste', this.onPaste);
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
|
bus.$off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
bus.$off(
|
||||||
|
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||||
|
this.onNewConversationModalActive
|
||||||
|
);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleInsert(article) {
|
||||||
|
const { url, title } = article;
|
||||||
|
if (this.isRichEditorEnabled) {
|
||||||
|
// Removing empty lines from the title
|
||||||
|
const lines = title.split('\n');
|
||||||
|
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
||||||
|
const filteredMarkdown = nonEmptyLines.join(' ');
|
||||||
|
bus.$emit(
|
||||||
|
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||||
|
`[${filteredMarkdown}](${url})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.addIntoEditor(
|
||||||
|
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$track(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
|
||||||
|
},
|
||||||
toggleRichContentEditor() {
|
toggleRichContentEditor() {
|
||||||
this.updateUISettings({
|
this.updateUISettings({
|
||||||
display_rich_content_editor: !this.showRichContentEditor,
|
display_rich_content_editor: !this.showRichContentEditor,
|
||||||
@@ -753,6 +813,7 @@ export default {
|
|||||||
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
||||||
channelType: this.channelType,
|
channelType: this.channelType,
|
||||||
signatureEnabled: this.sendWithSignature,
|
signatureEnabled: this.sendWithSignature,
|
||||||
|
hasReplyTo: !!this.inReplyTo?.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async onSendReply() {
|
async onSendReply() {
|
||||||
@@ -803,10 +864,18 @@ export default {
|
|||||||
this.hideWhatsappTemplatesModal();
|
this.hideWhatsappTemplatesModal();
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
|
if (this.sendWithSignature && !this.private) {
|
||||||
|
// if signature is enabled, append it to the message
|
||||||
|
// appendSignature ensures that the signature is not duplicated
|
||||||
|
// so we don't need to check if the signature is already present
|
||||||
|
message = appendSignature(message, this.signatureToApply);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedMessage = replaceVariablesInMessage({
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
message,
|
message,
|
||||||
variables: this.messageVariables,
|
variables: this.messageVariables,
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
this.message = updatedMessage;
|
this.message = updatedMessage;
|
||||||
@@ -829,22 +898,22 @@ export default {
|
|||||||
clearEditorSelection() {
|
clearEditorSelection() {
|
||||||
this.updateEditorSelectionWith = '';
|
this.updateEditorSelectionWith = '';
|
||||||
},
|
},
|
||||||
insertEmoji(emoji, selectionStart, selectionEnd) {
|
insertIntoTextEditor(text, selectionStart, selectionEnd) {
|
||||||
const { message } = this;
|
const { message } = this;
|
||||||
const newMessage =
|
const newMessage =
|
||||||
message.slice(0, selectionStart) +
|
message.slice(0, selectionStart) +
|
||||||
emoji +
|
text +
|
||||||
message.slice(selectionEnd, message.length);
|
message.slice(selectionEnd, message.length);
|
||||||
this.message = newMessage;
|
this.message = newMessage;
|
||||||
},
|
},
|
||||||
emojiOnClick(emoji) {
|
addIntoEditor(content) {
|
||||||
if (this.showRichContentEditor) {
|
if (this.showRichContentEditor) {
|
||||||
this.updateEditorSelectionWith = emoji;
|
this.updateEditorSelectionWith = content;
|
||||||
this.onFocus();
|
this.onFocus();
|
||||||
}
|
}
|
||||||
if (!this.showRichContentEditor) {
|
if (!this.showRichContentEditor) {
|
||||||
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
||||||
this.insertEmoji(emoji, selectionStart, selectionEnd);
|
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
@@ -855,6 +924,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
|
this.resetReplyToMessage();
|
||||||
},
|
},
|
||||||
clearEmailField() {
|
clearEmailField() {
|
||||||
this.ccEmails = '';
|
this.ccEmails = '';
|
||||||
@@ -947,6 +1017,19 @@ export default {
|
|||||||
(item, index) => itemIndex !== index
|
(item, index) => itemIndex !== index
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setReplyToInPayload(payload) {
|
||||||
|
if (this.inReplyTo?.id) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
contentAttributes: {
|
||||||
|
...payload.contentAttributes,
|
||||||
|
in_reply_to: this.inReplyTo.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
getMessagePayloadForWhatsapp(message) {
|
getMessagePayloadForWhatsapp(message) {
|
||||||
const multipleMessagePayload = [];
|
const multipleMessagePayload = [];
|
||||||
|
|
||||||
@@ -956,39 +1039,41 @@ export default {
|
|||||||
const attachedFile = this.globalConfig.directUploadsEnabled
|
const attachedFile = this.globalConfig.directUploadsEnabled
|
||||||
? attachment.blobSignedId
|
? attachment.blobSignedId
|
||||||
: attachment.resource.file;
|
: attachment.resource.file;
|
||||||
const attachmentPayload = {
|
let attachmentPayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
files: [attachedFile],
|
files: [attachedFile],
|
||||||
private: false,
|
private: false,
|
||||||
message: caption,
|
message: caption,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
|
||||||
multipleMessagePayload.push(attachmentPayload);
|
multipleMessagePayload.push(attachmentPayload);
|
||||||
caption = '';
|
caption = '';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const messagePayload = {
|
let messagePayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
message,
|
message,
|
||||||
private: false,
|
private: false,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||||
|
|
||||||
multipleMessagePayload.push(messagePayload);
|
multipleMessagePayload.push(messagePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return multipleMessagePayload;
|
return multipleMessagePayload;
|
||||||
},
|
},
|
||||||
getMessagePayload(message) {
|
getMessagePayload(message) {
|
||||||
const messagePayload = {
|
let messagePayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
message,
|
message,
|
||||||
private: this.isPrivate,
|
private: this.isPrivate,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||||
if (this.inReplyTo) {
|
|
||||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attachedFiles && this.attachedFiles.length) {
|
if (this.attachedFiles && this.attachedFiles.length) {
|
||||||
messagePayload.files = [];
|
messagePayload.files = [];
|
||||||
@@ -1060,6 +1145,39 @@ export default {
|
|||||||
this.bccEmails = bcc.join(', ');
|
this.bccEmails = bcc.join(', ');
|
||||||
this.toEmails = to.join(', ');
|
this.toEmails = to.join(', ');
|
||||||
},
|
},
|
||||||
|
fetchAndSetReplyTo() {
|
||||||
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||||
|
const replyToMessageId = LocalStorage.getFromJsonStore(
|
||||||
|
replyStorageKey,
|
||||||
|
this.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
this.inReplyTo = this.currentChat?.messages?.find(message => {
|
||||||
|
if (message.id === replyToMessageId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetReplyToMessage() {
|
||||||
|
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||||
|
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
|
||||||
|
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
|
||||||
|
},
|
||||||
|
onNewConversationModalActive(isActive) {
|
||||||
|
// Issue is if the new conversation modal is open and we drag and drop the file
|
||||||
|
// then the file is not getting attached to the new conversation modal
|
||||||
|
// and it is getting attached to the current conversation reply box
|
||||||
|
// so to fix this we are removing the drag and drop event listener from the current conversation reply box
|
||||||
|
// When new conversation modal is open
|
||||||
|
this.newConversationModalActive = isActive;
|
||||||
|
},
|
||||||
|
onSearchPopoverClose() {
|
||||||
|
this.showArticleSearchPopover = false;
|
||||||
|
},
|
||||||
|
toggleInsertArticle() {
|
||||||
|
this.showArticleSearchPopover = !this.showArticleSearchPopover;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1078,7 +1196,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reply-box {
|
.reply-box {
|
||||||
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
@apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
|
||||||
|
|
||||||
&.is-private {
|
&.is-private {
|
||||||
@apply bg-yellow-50 dark:bg-yellow-200;
|
@apply bg-yellow-50 dark:bg-yellow-200;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
|
||||||
|
>
|
||||||
|
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
|
||||||
|
<div class="flex-grow gap-1 mt-px text-xs truncate">
|
||||||
|
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }}
|
||||||
|
<message-preview
|
||||||
|
:message="message"
|
||||||
|
:show-message-type="false"
|
||||||
|
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
variant="clear"
|
||||||
|
size="tiny"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click.stop="$emit('dismiss')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { MessagePreview },
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// TODO: Remove this
|
||||||
|
// override for dashboard/assets/scss/widgets/_reply-box.scss
|
||||||
|
.reply-editor {
|
||||||
|
.icon {
|
||||||
|
margin-right: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button @click="sendMessage">
|
<woot-button type="button" @click="sendMessage">
|
||||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -57,17 +57,6 @@
|
|||||||
@mouseenter="isHovered = true"
|
@mouseenter="isHovered = true"
|
||||||
@mouseleave="isHovered = false"
|
@mouseleave="isHovered = false"
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
|
|
||||||
@click="onTweetReply"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
|
||||||
icon="arrow-reply"
|
|
||||||
class="action--icon cursor-pointer"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
@@ -77,7 +66,7 @@
|
|||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
||||||
icon="open"
|
icon="open"
|
||||||
class="action--icon cursor-pointer"
|
class="cursor-pointer action--icon"
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -86,7 +75,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import timeMixin from '../../../../mixins/time';
|
||||||
@@ -126,10 +114,6 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
hasInstagramStory: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
messageType: {
|
messageType: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
@@ -208,24 +192,46 @@ export default {
|
|||||||
if (!this.showStatusIndicators) {
|
if (!this.showStatusIndicators) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Messages will be marked as sent for the Email channel if they have a source ID.
|
||||||
if (this.isAnEmailChannel) {
|
if (this.isAnEmailChannel) {
|
||||||
return !!this.sourceId;
|
return !!this.sourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAWhatsAppChannel) {
|
if (
|
||||||
|
this.isAWhatsAppChannel ||
|
||||||
|
this.isATwilioChannel ||
|
||||||
|
this.isAFacebookInbox ||
|
||||||
|
this.isASmsInbox ||
|
||||||
|
this.isATelegramChannel
|
||||||
|
) {
|
||||||
return this.sourceId && this.isSent;
|
return this.sourceId && this.isSent;
|
||||||
}
|
}
|
||||||
|
// All messages will be mark as sent for the Line channel, as there is no source ID.
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
showDeliveredIndicator() {
|
showDeliveredIndicator() {
|
||||||
if (!this.showStatusIndicators) {
|
if (!this.showStatusIndicators) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
if (this.isAWhatsAppChannel) {
|
this.isAWhatsAppChannel ||
|
||||||
|
this.isATwilioChannel ||
|
||||||
|
this.isASmsInbox ||
|
||||||
|
this.isAFacebookInbox
|
||||||
|
) {
|
||||||
return this.sourceId && this.isDelivered;
|
return this.sourceId && this.isDelivered;
|
||||||
}
|
}
|
||||||
|
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
|
||||||
|
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||||
|
return this.isSent;
|
||||||
|
}
|
||||||
|
if (this.isALineChannel) {
|
||||||
|
return this.isDelivered;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@@ -233,24 +239,21 @@ export default {
|
|||||||
if (!this.showStatusIndicators) {
|
if (!this.showStatusIndicators) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
this.isAWhatsAppChannel ||
|
||||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
this.isATwilioChannel ||
|
||||||
return contactLastSeenAt >= this.createdAt;
|
this.isAFacebookInbox
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (this.isAWhatsAppChannel) {
|
|
||||||
return this.sourceId && this.isRead;
|
return this.sourceId && this.isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isAWebWidgetInbox || this.isAPIInbox) {
|
||||||
|
return this.isRead;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
onTweetReply() {
|
|
||||||
bus.$emit(BUS_EVENTS.SET_TWEET_REPLY, this.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image message-text__wrap">
|
<div class="image message-text__wrap">
|
||||||
<img :src="url" @click="onClick" @error="onImgError()" />
|
<img :src="url" @click="onClick" @error="$emit('error')" />
|
||||||
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
|
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
|
||||||
<img :src="url" class="modal-image skip-context-menu" />
|
<img :src="url" class="modal-image skip-context-menu" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
@@ -28,9 +28,6 @@ export default {
|
|||||||
onClick() {
|
onClick() {
|
||||||
this.show = true;
|
this.show = true;
|
||||||
},
|
},
|
||||||
onImgError() {
|
|
||||||
this.$emit('error');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
v-if="isImage && !isImageError"
|
v-if="isImage && !isImageError"
|
||||||
:src="attachment.data_url"
|
:src="attachment.data_url"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@error="onImgError()"
|
@error="onImgError"
|
||||||
/>
|
/>
|
||||||
<video
|
<video
|
||||||
v-if="isVideo"
|
v-if="isVideo"
|
||||||
:src="attachment.data_url"
|
:src="attachment.data_url"
|
||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
|
@error="onImgError"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
/>
|
/>
|
||||||
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
:show.sync="show"
|
:show.sync="show"
|
||||||
:attachment="attachment"
|
:attachment="attachment"
|
||||||
:all-attachments="filteredCurrentChatAttachments"
|
:all-attachments="filteredCurrentChatAttachments"
|
||||||
@error="onImgError()"
|
@error="onImgError"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<bubble-image
|
||||||
|
v-if="!hasImgStoryError"
|
||||||
|
:url="storyUrl"
|
||||||
|
@error="onImageLoadError"
|
||||||
|
/>
|
||||||
|
<bubble-video
|
||||||
|
v-else-if="!hasVideoStoryError"
|
||||||
|
:url="storyUrl"
|
||||||
|
@error="onVideoLoadError"
|
||||||
|
/>
|
||||||
|
<instagram-story-error-place-holder v-else />
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import BubbleImage from './Image.vue';
|
||||||
|
import BubbleVideo from './Video.vue';
|
||||||
|
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BubbleImage,
|
||||||
|
BubbleVideo,
|
||||||
|
InstagramStoryErrorPlaceHolder,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
storyUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasImgStoryError: false,
|
||||||
|
hasVideoStoryError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImageLoadError() {
|
||||||
|
this.hasImgStoryError = true;
|
||||||
|
this.emitError();
|
||||||
|
},
|
||||||
|
onVideoLoadError() {
|
||||||
|
this.hasVideoStoryError = true;
|
||||||
|
this.emitError();
|
||||||
|
},
|
||||||
|
emitError() {
|
||||||
|
this.$emit('error');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="document-error" size="32" />
|
||||||
|
<p class="mb-0 text-slate-700 dark:text-slate-75">
|
||||||
|
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {};
|
||||||
|
</script>
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<blockquote
|
||||||
|
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
|
||||||
|
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
|
||||||
|
</blockquote>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import InstagramStory from './InstagramStory.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { InstagramStory },
|
||||||
|
props: {
|
||||||
|
storyUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasImgStoryError: false,
|
||||||
|
hasVideoStoryError: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onImageLoadError() {
|
||||||
|
this.hasImgStoryError = true;
|
||||||
|
},
|
||||||
|
onVideoLoadError() {
|
||||||
|
this.hasVideoStoryError = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
|
||||||
|
:class="{
|
||||||
|
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
|
||||||
|
messageType === MESSAGE_TYPE.INCOMING,
|
||||||
|
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||||
|
'-mx-2': !parentHasAttachments,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<message-preview
|
||||||
|
:message="message"
|
||||||
|
:show-message-type="false"
|
||||||
|
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||||
|
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReplyTo',
|
||||||
|
components: {
|
||||||
|
MessagePreview,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
parentHasAttachments: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { MESSAGE_TYPE };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user