diff --git a/.circleci/config.yml b/.circleci/config.yml
index ce22c7c86..7b72fb0c0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- - image: circleci/ruby:2.6.5-node-browsers
+ - image: circleci/ruby:2.7.0-node-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 0bf089f4b..761ad4d7a 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -26,3 +26,7 @@ exclude_patterns:
- "node_modules/**/*"
- "lib/tasks/auto_annotate_models.rake"
- "app/test-matchers.js"
+ - "docs/*"
+ - "**/*.md"
+ - "**/*.yml"
+ - "app/javascript/dashboard/i18n/locale"
diff --git a/.env.example b/.env.example
index e6075e55e..bc36e8588 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,19 @@
-SECRET_KEY_BASE=
+# Used to verify the integrity of signed cookies. so ensure a secure value is set
+SECRET_KEY_BASE=replace_with_lengthy_secure_hex
-#redis config
+# Replace with the URL you are planning to use for your app
+FRONTEND_URL=http://0.0.0.0:3000
+
+# Force all access to the app over SSL, default is set to false
+FORCE_SSL=false
+
+# This lets you control new sign ups on your chatwoot installation
+# true : default option, allows sign ups
+# false : disables all the end points related to sign ups
+# api_only: disables the UI for signup, but you can create sign ups via the account apis
+ENABLE_ACCOUNT_SIGNUP=true
+
+# Redis config
REDIS_URL=redis://redis:6379
# If you are using docker-compose, set this variable's value to be any string,
# which will be the password for the redis service running inside the docker-compose
@@ -14,18 +27,7 @@ POSTGRES_PASSWORD=
RAILS_ENV=development
RAILS_MAX_THREADS=5
-#fb app
-FB_VERIFY_TOKEN=
-FB_APP_SECRET=
-FB_APP_ID=
-
-#twitter app
-TWITTER_APP_ID=
-TWITTER_CONSUMER_KEY=
-TWITTER_CONSUMER_SECRET=
-TWITTER_ENVIRONMENT=
-
-#mail
+# Mail outgoing
MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025
SMTP_DOMAIN=chatwoot.com
@@ -37,23 +39,47 @@ SMTP_PASSWORD=
SMTP_AUTHENTICATION=
SMTP_ENABLE_STARTTLS_AUTO=
-#misc
-FRONTEND_URL=http://0.0.0.0:3000
+# Mail Incoming
+# Use one of the following based on the email ingress service
+# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
+RAILS_INBOUND_EMAIL_PASSWORD=
+MAILGUN_INGRESS_SIGNING_KEY=
+MANDRILL_INGRESS_API_KEY=
+
+# Storage
ACTIVE_STORAGE_SERVICE=local
-#s3
+# Amazon S3
S3_BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
-#sentry
+# Sentry
SENTRY_DSN=
+# Log settings
+# Disable if you want to write logs to a file
+RAILS_LOG_TO_STDOUT=true
+LOG_LEVEL=info
+LOG_SIZE=500
+
# Credentials to access sidekiq dashboard in production
SIDEKIQ_AUTH_USERNAME=
SIDEKIQ_AUTH_PASSWORD=
+### This environment variables are only required if you are setting up social media channels
+#facebook
+FB_VERIFY_TOKEN=
+FB_APP_SECRET=
+FB_APP_ID=
+
+# Twitter
+TWITTER_APP_ID=
+TWITTER_CONSUMER_KEY=
+TWITTER_CONSUMER_SECRET=
+TWITTER_ENVIRONMENT=
+
#### This environment variables are only required in hosted version which has billing
ENABLE_BILLING=
diff --git a/.eslintrc.js b/.eslintrc.js
index c6c52bb34..9b99b9b61 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,8 +1,8 @@
module.exports = {
- extends: ['airbnb/base', 'prettier', 'plugin:vue/recommended'],
+ extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
parserOptions: {
parser: 'babel-eslint',
- ecmaVersion: 2017,
+ ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['html', 'prettier', 'babel'],
@@ -24,10 +24,12 @@ module.exports = {
'multiline': {
'max': 1,
'allowFirstLine': false
- }
+ },
}],
'vue/html-self-closing': 'off',
- "vue/no-v-html": 'off'
+ "vue/no-v-html": 'off',
+ 'import/extensions': ['off']
+
},
settings: {
'import/resolver': {
diff --git a/.gitignore b/.gitignore
index 7192ba9d9..d058d20d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,10 @@ public/packs*
*.swo
*.un~
.jest-cache
+
+#VS Code files
+.vscode
+
# ignore jetbrains IDE files
.idea
@@ -48,4 +52,6 @@ coverage
# ignore packages
node_modules
-package-lock.json
\ No newline at end of file
+package-lock.json
+
+*.dump
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..66df3b7ab
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+12.16.1
diff --git a/.rubocop.yml b/.rubocop.yml
index 7c73f0191..48d234ba0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -4,6 +4,10 @@ require:
- rubocop-rspec
inherit_from: .rubocop_todo.yml
+Lint/RaiseException:
+ Enabled: true
+Lint/StructNewOverride:
+ Enabled: true
Layout/LineLength:
Max: 150
Metrics/ClassLength:
@@ -16,6 +20,12 @@ Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
Enabled: false
+Style/HashEachMethods:
+ Enabled: true
+Style/HashTransformKeys:
+ Enabled: true
+Style/HashTransformValues:
+ Enabled: true
Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
@@ -41,14 +51,58 @@ RSpec/NestedGroups:
Max: 4
RSpec/MessageSpies:
Enabled: false
+Metrics/MethodLength:
+ Exclude:
+ - 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
+Rails/CreateTableWithTimestamps:
+ Exclude:
+ - 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb'
+Style/GuardClause:
+ Exclude:
+ - 'app/builders/account_builder.rb'
+ - 'app/models/attachment.rb'
+ - 'app/models/message.rb'
+ - 'lib/webhooks/chargebee.rb'
+ - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
+Metrics/AbcSize:
+ Exclude:
+ - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
+Metrics/CyclomaticComplexity:
+ Exclude:
+ - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
+Rails/ReversibleMigration:
+ Exclude:
+ - 'db/migrate/20161025070152_removechannelsfrommodels.rb'
+ - 'db/migrate/20161025070645_remchannel.rb'
+ - 'db/migrate/20161025070645_remchannel.rb'
+ - 'db/migrate/20161110102609_removeinboxid.rb'
+ - 'db/migrate/20170519091539_add_avatar_to_fb.rb'
+ - 'db/migrate/20191020085608_rename_old_tables.rb'
+ - 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
+ - 'db/migrate/20191130164019_add_template_type_to_messages.rb'
+Rails/BulkChangeTable:
+ Exclude:
+ - 'db/migrate/20161025070152_removechannelsfrommodels.rb'
+ - 'db/migrate/20200121190901_create_account_users.rb'
+ - 'db/migrate/20170211092540_notnullableusers.rb'
+ - 'db/migrate/20170403095203_contactadder.rb'
+ - 'db/migrate/20170406104018_add_default_status_conv.rb'
+ - 'db/migrate/20170511134418_latlong.rb'
+ - 'db/migrate/20191027054756_create_contact_inboxes.rb'
+ - 'db/migrate/20191130164019_add_template_type_to_messages.rb'
+Rails/UniqueValidationWithoutIndex:
+ Exclude:
+ - 'app/models/channel/twitter_profile.rb'
+ - 'app/models/webhook.rb'
AllCops:
Exclude:
- - db/*
- - bin/**/*
- - db/**/*
- - config/**/*
- - public/**/*
- - vendor/**/*
- - node_modules/**/*
- - lib/tasks/auto_annotate_models.rake
- - config/environments/**/*
+ - 'bin/**/*'
+ - 'db/schema.rb'
+ - 'config/**/*'
+ - 'public/**/*'
+ - 'vendor/**/*'
+ - 'node_modules/**/*'
+ - 'lib/tasks/auto_annotate_models.rake'
+ - 'config/environments/**/*'
+ - 'tmp/**/*'
+ - 'storage/**/*'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 17dfb84f9..06a287ae1 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -282,15 +282,6 @@ Style/GlobalVars:
Exclude:
- 'lib/redis/alfred.rb'
-# Offense count: 7
-# Configuration parameters: MinBodyLength.
-Style/GuardClause:
- Exclude:
- - 'app/builders/account_builder.rb'
- - 'app/models/attachment.rb'
- - 'app/models/message.rb'
- - 'lib/webhooks/chargebee.rb'
-
# Offense count: 4
Style/IdenticalConditionalBranches:
Exclude:
diff --git a/.ruby-version b/.ruby-version
index 57cf282eb..24ba9a38d 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.6.5
+2.7.0
diff --git a/.scss-lint.yml b/.scss-lint.yml
index dadb2c2cd..9f5f4fe10 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -82,7 +82,7 @@ linters:
enabled: true
ImportantRule:
- enabled: true
+ enabled: false
ImportPath:
enabled: true
@@ -252,7 +252,7 @@ linters:
enabled: false
UnnecessaryParentReference:
- enabled: true
+ enabled: false
UrlFormat:
enabled: true
diff --git a/Gemfile b/Gemfile
index d2f3465e2..9f4d3cf40 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-ruby '2.6.5'
+ruby '2.7.0'
##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors'
@@ -17,6 +17,7 @@ gem 'jbuilder'
gem 'kaminari'
gem 'responders'
gem 'rest-client'
+gem 'telephone_number'
gem 'time_diff'
gem 'tzinfo-data'
gem 'valid_email2'
@@ -25,11 +26,12 @@ gem 'uglifier'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
-gem 'azure-storage', require: false
+gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'mini_magick'
##-- gems for database --#
+gem 'groupdate'
gem 'pg'
gem 'redis'
gem 'redis-namespace'
@@ -61,9 +63,9 @@ gem 'chargebee'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'telegram-bot-ruby'
+gem 'twilio-ruby', '~> 5.32.0'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
-
# facebook client
gem 'koala'
# Random name generator
@@ -78,11 +80,17 @@ gem 'sentry-raven'
##-- background job processing --##
gem 'sidekiq'
+##-- used for single column multiple binary flags in notification settings/feature flagging --##
+gem 'flag_shih_tzu'
+
group :development do
gem 'annotate'
gem 'bullet'
gem 'letter_opener'
gem 'web-console'
+
+ # used in swagger build
+ gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
end
group :development, :test do
@@ -93,7 +101,7 @@ group :development, :test do
gem 'factory_bot_rails'
gem 'faker'
gem 'listen'
- gem 'mock_redis'
+ gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
gem 'pry-rails'
gem 'rspec-rails', '~> 4.0.0.beta2'
gem 'rubocop', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 9bb07458f..577676cf5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,65 +1,80 @@
GIT
remote: https://github.com/chatwoot/twitty
- revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
+ revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
specs:
twitty (0.1.0)
oauth
+GIT
+ remote: https://github.com/sds/mock_redis
+ revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
+ ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
+ specs:
+ mock_redis (0.22.0)
+
+GIT
+ remote: https://github.com/tzmfreedom/json_refs
+ revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
+ ref: e32deb0
+ specs:
+ json_refs (0.1.2)
+ hana
+
GEM
remote: https://rubygems.org/
specs:
- action-cable-testing (0.6.0)
+ action-cable-testing (0.6.1)
actioncable (>= 5.0)
- actioncable (6.0.2.1)
- actionpack (= 6.0.2.1)
+ actioncable (6.0.2.2)
+ actionpack (= 6.0.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.0.2.1)
- actionpack (= 6.0.2.1)
- activejob (= 6.0.2.1)
- activerecord (= 6.0.2.1)
- activestorage (= 6.0.2.1)
- activesupport (= 6.0.2.1)
+ actionmailbox (6.0.2.2)
+ actionpack (= 6.0.2.2)
+ activejob (= 6.0.2.2)
+ activerecord (= 6.0.2.2)
+ activestorage (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
mail (>= 2.7.1)
- actionmailer (6.0.2.1)
- actionpack (= 6.0.2.1)
- actionview (= 6.0.2.1)
- activejob (= 6.0.2.1)
+ actionmailer (6.0.2.2)
+ actionpack (= 6.0.2.2)
+ actionview (= 6.0.2.2)
+ activejob (= 6.0.2.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.0.2.1)
- actionview (= 6.0.2.1)
- activesupport (= 6.0.2.1)
+ actionpack (6.0.2.2)
+ actionview (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.0.2.1)
- actionpack (= 6.0.2.1)
- activerecord (= 6.0.2.1)
- activestorage (= 6.0.2.1)
- activesupport (= 6.0.2.1)
+ actiontext (6.0.2.2)
+ actionpack (= 6.0.2.2)
+ activerecord (= 6.0.2.2)
+ activestorage (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
nokogiri (>= 1.8.5)
- actionview (6.0.2.1)
- activesupport (= 6.0.2.1)
+ actionview (6.0.2.2)
+ activesupport (= 6.0.2.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.0.2.1)
- activesupport (= 6.0.2.1)
+ activejob (6.0.2.2)
+ activesupport (= 6.0.2.2)
globalid (>= 0.3.6)
- activemodel (6.0.2.1)
- activesupport (= 6.0.2.1)
- activerecord (6.0.2.1)
- activemodel (= 6.0.2.1)
- activesupport (= 6.0.2.1)
- activestorage (6.0.2.1)
- actionpack (= 6.0.2.1)
- activejob (= 6.0.2.1)
- activerecord (= 6.0.2.1)
+ activemodel (6.0.2.2)
+ activesupport (= 6.0.2.2)
+ activerecord (6.0.2.2)
+ activemodel (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
+ activestorage (6.0.2.2)
+ actionpack (= 6.0.2.2)
+ activejob (= 6.0.2.2)
+ activerecord (= 6.0.2.2)
marcel (~> 0.3.1)
- activesupport (6.0.2.1)
+ activesupport (6.0.2.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -69,46 +84,44 @@ GEM
activerecord (>= 5.0, < 6.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
- annotate (3.0.3)
+ annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.3)
- aws-eventstream (1.0.3)
- aws-partitions (1.269.0)
- aws-sdk-core (3.89.1)
- aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-eventstream (1.1.0)
+ aws-partitions (1.296.0)
+ aws-sdk-core (3.94.0)
+ aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.28.0)
+ aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.60.1)
+ aws-sdk-s3 (1.61.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.1.0)
+ aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- azure-core (0.1.15)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- nokogiri (~> 1.6)
- azure-storage (0.15.0.preview)
- azure-core (~> 0.1)
- faraday (~> 0.9)
- faraday_middleware (~> 0.10)
- nokogiri (~> 1.6, >= 1.6.8)
+ azure-storage-blob (2.0.0)
+ azure-storage-common (~> 2.0)
+ nokogiri (~> 1.10.4)
+ azure-storage-common (2.0.1)
+ faraday (~> 1.0)
+ faraday_middleware (~> 1.0.0.rc1)
+ nokogiri (~> 1.10.4)
bcrypt (3.1.13)
bindex (0.8.1)
- bootsnap (1.4.5)
+ bootsnap (1.4.6)
msgpack (~> 1.0)
- brakeman (4.7.2)
- browser (3.0.3)
+ brakeman (4.8.1)
+ browser (4.0.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
@@ -119,13 +132,13 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.1.1)
- chargebee (2.7.3)
+ chargebee (2.7.5)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- concurrent-ruby (1.1.5)
+ concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crass (1.0.6)
declarative (0.0.10)
@@ -143,7 +156,7 @@ GEM
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1)
diff-lcs (1.3)
- digest-crc (0.4.1)
+ digest-crc (0.5.1)
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@@ -157,22 +170,23 @@ GEM
facebook-messenger (1.4.1)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
- factory_bot (5.1.1)
+ factory_bot (5.1.2)
activesupport (>= 4.2.0)
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
- faker (2.10.1)
+ faker (2.11.0)
i18n (>= 1.6, < 2)
- faraday (0.17.3)
+ faraday (1.0.1)
multipart-post (>= 1.2, < 3)
- faraday_middleware (0.14.0)
- faraday (>= 0.7.4, < 1.0)
+ faraday_middleware (1.0.0)
+ faraday (~> 1.0)
ffi (1.12.2)
- foreman (0.87.0)
+ flag_shih_tzu (0.3.23)
+ foreman (0.87.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
- google-api-client (0.36.4)
+ google-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
@@ -183,29 +197,32 @@ GEM
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
- google-cloud-env (1.3.0)
- faraday (~> 0.11)
+ google-cloud-env (1.3.1)
+ faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
- google-cloud-storage (1.25.1)
+ google-cloud-storage (1.26.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
- googleauth (0.10.0)
- faraday (~> 0.12)
+ googleauth (0.12.0)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
- signet (~> 0.12)
+ signet (~> 0.14)
+ groupdate (5.0.0)
+ activesupport (>= 5)
haikunator (1.1.0)
+ hana (1.3.5)
hashie (4.1.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
- httparty (0.17.3)
+ httparty (0.18.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
@@ -214,11 +231,11 @@ GEM
ice_nine (0.11.2)
inflecto (0.0.2)
jaro_winkler (1.5.4)
- jbuilder (2.9.1)
- activesupport (>= 4.2.0)
+ jbuilder (2.10.0)
+ activesupport (>= 5.0.0)
jmespath (1.4.0)
json (2.3.0)
- json_pure (2.2.0)
+ json_pure (2.3.0)
jwt (2.2.1)
kaminari (1.2.0)
activesupport (>= 4.1.0)
@@ -236,14 +253,14 @@ GEM
addressable
faraday
json (>= 1.8)
- launchy (2.4.3)
- addressable (~> 2.3)
+ launchy (2.5.0)
+ addressable (~> 2.7)
letter_opener (1.7.0)
launchy (~> 2.2)
listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
- loofah (2.4.0)
+ loofah (2.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -251,7 +268,7 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
- method_source (0.9.2)
+ method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
@@ -260,35 +277,34 @@ GEM
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
- mock_redis (0.22.0)
- msgpack (1.3.1)
+ msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
netrc (0.11.0)
nightfury (1.0.1)
nio4r (2.5.2)
- nokogiri (1.10.7)
+ nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
oauth (0.5.4)
orm_adapter (0.5.0)
- os (1.0.1)
+ os (1.1.0)
parallel (1.19.1)
- parser (2.7.0.2)
+ parser (2.7.1.1)
ast (~> 2.4.0)
- pg (1.2.2)
- pry (0.12.2)
- coderay (~> 1.1.0)
- method_source (~> 0.9.0)
+ pg (1.2.3)
+ pry (0.13.1)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
- public_suffix (4.0.3)
- puma (4.3.1)
+ public_suffix (4.0.4)
+ puma (4.3.3)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
- rack (2.1.2)
- rack-cache (1.11.0)
+ rack (2.2.2)
+ rack-cache (1.11.1)
rack (>= 0.4)
rack-cors (1.1.1)
rack (>= 2.0.0)
@@ -298,29 +314,29 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.0.2.1)
- actioncable (= 6.0.2.1)
- actionmailbox (= 6.0.2.1)
- actionmailer (= 6.0.2.1)
- actionpack (= 6.0.2.1)
- actiontext (= 6.0.2.1)
- actionview (= 6.0.2.1)
- activejob (= 6.0.2.1)
- activemodel (= 6.0.2.1)
- activerecord (= 6.0.2.1)
- activestorage (= 6.0.2.1)
- activesupport (= 6.0.2.1)
+ rails (6.0.2.2)
+ actioncable (= 6.0.2.2)
+ actionmailbox (= 6.0.2.2)
+ actionmailer (= 6.0.2.2)
+ actionpack (= 6.0.2.2)
+ actiontext (= 6.0.2.2)
+ actionview (= 6.0.2.2)
+ activejob (= 6.0.2.2)
+ activemodel (= 6.0.2.2)
+ activerecord (= 6.0.2.2)
+ activestorage (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
bundler (>= 1.3.0)
- railties (= 6.0.2.1)
+ railties (= 6.0.2.2)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
- railties (6.0.2.1)
- actionpack (= 6.0.2.1)
- activesupport (= 6.0.2.1)
+ railties (6.0.2.2)
+ actionpack (= 6.0.2.2)
+ activesupport (= 6.0.2.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@@ -335,7 +351,7 @@ GEM
redis-rack-cache (2.2.1)
rack-cache (>= 1.10, < 2)
redis-store (>= 1.6, < 2)
- redis-store (1.8.1)
+ redis-store (1.8.2)
redis (>= 4, < 5)
representable (3.0.4)
declarative (< 0.1.0)
@@ -350,15 +366,16 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
+ rexml (3.2.4)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
- rspec-expectations (3.9.0)
+ rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
- rspec-rails (4.0.0.beta4)
+ rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
@@ -367,19 +384,21 @@ GEM
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.2)
- rubocop (0.79.0)
+ rubocop (0.81.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
+ rexml
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 1.4.0, < 1.7)
+ unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.2)
rubocop (>= 0.71.0)
- rubocop-rails (2.4.2)
+ rubocop-rails (2.5.2)
+ activesupport
rack (>= 1.1)
rubocop (>= 0.72.0)
- rubocop-rspec (1.37.1)
+ rubocop-rspec (1.38.1)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
sass (3.7.4)
@@ -387,25 +406,26 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- scout_apm (2.6.6)
+ scout_apm (2.6.7)
parser
scss_lint (0.59.0)
sass (~> 3.5, >= 3.5.5)
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
- sentry-raven (2.13.0)
- faraday (>= 0.7.6, < 1.0)
- shoulda-matchers (4.2.0)
+ semantic_range (2.3.0)
+ sentry-raven (3.0.0)
+ faraday (>= 1.0)
+ shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
- sidekiq (6.0.4)
+ sidekiq (6.0.6)
connection_pool (>= 2.2.2)
- rack (>= 2.0.0)
+ rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
- signet (0.12.0)
+ signet (0.14.0)
addressable (~> 2.3)
- faraday (~> 0.9)
+ faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.17.1)
@@ -428,12 +448,17 @@ GEM
faraday
inflecto
virtus
+ telephone_number (1.4.6)
thor (0.20.3)
thread_safe (0.3.6)
time_diff (0.3.0)
activesupport
i18n
- tzinfo (1.2.6)
+ twilio-ruby (5.32.0)
+ faraday (~> 1.0.0)
+ jwt (>= 1.5, <= 2.5)
+ nokogiri (>= 1.6, < 2.0)
+ tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
@@ -442,10 +467,10 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.6)
- unicode-display_width (1.6.1)
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.7.0)
uniform_notifier (1.13.0)
- valid_email2 (3.1.3)
+ valid_email2 (3.2.2)
activemodel (>= 3.2)
mail (~> 2.5)
virtus (1.0.5)
@@ -460,15 +485,16 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
- webpacker (4.2.2)
- activesupport (>= 4.2)
+ webpacker (5.0.1)
+ activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
- railties (>= 4.2)
+ railties (>= 5.2)
+ semantic_range (>= 2.3.0)
websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
wisper (2.0.0)
- zeitwerk (2.2.2)
+ zeitwerk (2.3.0)
PLATFORMS
ruby
@@ -479,7 +505,7 @@ DEPENDENCIES
annotate
attr_extras
aws-sdk-s3
- azure-storage
+ azure-storage-blob
bootsnap
brakeman
browser
@@ -493,18 +519,21 @@ DEPENDENCIES
facebook-messenger
factory_bot_rails
faker
+ flag_shih_tzu
foreman
google-cloud-storage
+ groupdate
haikunator
hashie
jbuilder
+ json_refs!
jwt
kaminari
koala
letter_opener
listen
mini_magick
- mock_redis
+ mock_redis!
nightfury
pg
pry-rails
@@ -532,7 +561,9 @@ DEPENDENCIES
spring
spring-watcher-listen
telegram-bot-ruby
+ telephone_number
time_diff
+ twilio-ruby (~> 5.32.0)
twitty!
tzinfo-data
uglifier
@@ -542,7 +573,7 @@ DEPENDENCIES
wisper (= 2.0.0)
RUBY VERSION
- ruby 2.6.5p114
+ ruby 2.7.0p0
BUNDLED WITH
- 2.0.2
+ 2.1.2
diff --git a/README.md b/README.md
index 117a1fc9e..e71b8c6d1 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
A simple and elegant live chat software
An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.
@@ -23,7 +23,7 @@ ___
-![ChatUI progess](https://storage.googleapis.com/chatwoot-assets/dashboard-screen.png)
+![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png)
## Background
@@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
## Docker
-You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
-
-```bash
-docker pull chatwoot/chatwoot
-```
+Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
-Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer.
-
## Contributors ✨
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb
new file mode 100644
index 000000000..75af71a1f
--- /dev/null
+++ b/app/actions/contact_identify_action.rb
@@ -0,0 +1,47 @@
+class ContactIdentifyAction
+ pattr_initialize [:contact!, :params!]
+
+ def perform
+ ActiveRecord::Base.transaction do
+ @contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
+ @contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
+ update_contact
+ end
+ @contact
+ end
+
+ private
+
+ def account
+ @account ||= @contact.account
+ end
+
+ def existing_identified_contact
+ return if params[:identifier].blank?
+
+ @existing_identified_contact ||= Contact.where(account_id: account.id).find_by(identifier: params[:identifier])
+ end
+
+ def existing_email_contact
+ return if params[:email].blank?
+
+ @existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
+ end
+
+ def merge_contacts?(existing_contact, _contact)
+ existing_contact && existing_contact.id != @contact.id
+ end
+
+ def update_contact
+ @contact.update!(params.slice(:name, :email, :identifier))
+ ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
+ end
+
+ def merge_contact(base_contact, merge_contact)
+ ContactMergeAction.new(
+ account: account,
+ base_contact: base_contact,
+ mergee_contact: merge_contact
+ ).perform
+ end
+end
diff --git a/app/actions/contact_merge_action.rb b/app/actions/contact_merge_action.rb
index 61a26e185..343e78032 100644
--- a/app/actions/contact_merge_action.rb
+++ b/app/actions/contact_merge_action.rb
@@ -5,9 +5,11 @@ class ContactMergeAction
ActiveRecord::Base.transaction do
validate_contacts
merge_conversations
+ merge_messages
merge_contact_inboxes
remove_mergee_contact
end
+ @base_contact
end
private
@@ -15,7 +17,7 @@ class ContactMergeAction
def validate_contacts
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
- raise Exception, 'contact does not belong to the account'
+ raise StandardError, 'contact does not belong to the account'
end
def belongs_to_account?(contact)
@@ -26,6 +28,10 @@ class ContactMergeAction
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
+ def merge_messages
+ Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
+ end
+
def merge_contact_inboxes
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb
index c02bea13f..126eedce0 100644
--- a/app/builders/account_builder.rb
+++ b/app/builders/account_builder.rb
@@ -42,18 +42,26 @@ class AccountBuilder
def create_and_link_user
password = Time.now.to_i
- @user = @account.users.new(email: @email,
- password: password,
- password_confirmation: password,
- role: User.roles['administrator'],
- name: email_to_name(@email))
+ @user = User.new(email: @email,
+ password: password,
+ password_confirmation: password,
+ name: email_to_name(@email))
if @user.save!
+ link_user_to_account(@user, @account)
@user
else
raise UserErrors.new(errors: @user.errors)
end
end
+ def link_user_to_account(user, account)
+ AccountUser.create!(
+ account_id: account.id,
+ user_id: user.id,
+ role: AccountUser.roles['administrator']
+ )
+ end
+
def email_to_name(email)
name = email[/[^@]+/]
name.split('.').map(&:capitalize).join(' ')
diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb
new file mode 100644
index 000000000..9bb3ef3ac
--- /dev/null
+++ b/app/builders/contact_builder.rb
@@ -0,0 +1,38 @@
+class ContactBuilder
+ pattr_initialize [:source_id!, :inbox!, :contact_attributes!]
+
+ def perform
+ contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
+ return contact_inbox if contact_inbox
+
+ build_contact
+ end
+
+ private
+
+ def account
+ @account ||= inbox.account
+ end
+
+ def build_contact
+ ActiveRecord::Base.transaction do
+ contact = account.contacts.create!(
+ name: contact_attributes[:name],
+ phone_number: contact_attributes[:phone_number],
+ email: contact_attributes[:email],
+ identifier: contact_attributes[:identifier],
+ additional_attributes: contact_attributes[:additional_attributes]
+ )
+ contact_inbox = ::ContactInbox.create!(
+ contact_id: contact.id,
+ inbox_id: inbox.id,
+ source_id: source_id
+ )
+
+ ::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
+ contact_inbox
+ rescue StandardError => e
+ Rails.logger e
+ end
+ end
+end
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 6d3a7d39b..0eeba6b95 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -1,5 +1,3 @@
-require 'open-uri'
-
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
@@ -36,16 +34,14 @@ class Messages::MessageBuilder
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
- avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
- @contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
-
+ ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
- attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
+ attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb
index b75e8f84b..1b15f24c0 100644
--- a/app/builders/messages/outgoing/normal_builder.rb
+++ b/app/builders/messages/outgoing/normal_builder.rb
@@ -1,16 +1,31 @@
class Messages::Outgoing::NormalBuilder
+ include ::FileTypeHelper
attr_reader :message
def initialize(user, conversation, params)
- @content = params[:message]
- @private = ['1', 'true', 1, true].include? params[:private]
+ @content = params[:content]
+ @private = params[:private] || false
@conversation = conversation
@user = user
@fb_id = params[:fb_id]
+ @content_type = params[:content_type]
+ @items = params.to_unsafe_h&.dig(:content_attributes, :items)
+ @attachments = params[:attachments]
end
def perform
- @message = @conversation.messages.create!(message_params)
+ @message = @conversation.messages.build(message_params)
+ if @attachments.present?
+ @attachments.each do |uploaded_attachment|
+ attachment = @message.attachments.new(
+ account_id: @message.account_id,
+ file_type: file_type(uploaded_attachment&.content_type)
+ )
+ attachment.file.attach(uploaded_attachment)
+ end
+ end
+ @message.save
+ @message
end
private
@@ -22,8 +37,10 @@ class Messages::Outgoing::NormalBuilder
message_type: :outgoing,
content: @content,
private: @private,
- user_id: @user.id,
- source_id: @fb_id
+ user_id: @user&.id,
+ source_id: @fb_id,
+ content_type: @content_type,
+ items: @items
}
end
end
diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb
new file mode 100644
index 000000000..35f484f95
--- /dev/null
+++ b/app/builders/v2/report_builder.rb
@@ -0,0 +1,111 @@
+class V2::ReportBuilder
+ attr_reader :account, :params
+
+ def initialize(account, params)
+ @account = account
+ @params = params
+ end
+
+ def timeseries
+ send(params[:metric])
+ end
+
+ # For backward compatible with old report
+ def build
+ timeseries.each_with_object([]) do |p, arr|
+ arr << { value: p[1], timestamp: p[0].to_time.to_i }
+ end
+ end
+
+ def summary
+ {
+ conversations_count: conversations_count.values.sum,
+ incoming_messages_count: incoming_messages_count.values.sum,
+ outgoing_messages_count: outgoing_messages_count.values.sum,
+ avg_first_response_time: avg_first_response_time_summary,
+ avg_resolution_time: avg_resolution_time_summary,
+ resolutions_count: resolutions_count.values.sum
+ }
+ end
+
+ private
+
+ def scope
+ return account if params[:type].match?('account')
+ return inbox if params[:type].match?('inbox')
+ return user if params[:type].match?('agent')
+ end
+
+ def inbox
+ @inbox ||= account.inboxes.where(id: params[:id]).first
+ end
+
+ def user
+ @user ||= account.users.where(id: params[:id]).first
+ end
+
+ def conversations_count
+ scope.conversations
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .count
+ end
+
+ # unscoped removes all scopes added to a model previously
+ def incoming_messages_count
+ scope.messages.unscoped.where(account_id: account.id).incoming
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .count
+ end
+
+ def outgoing_messages_count
+ scope.messages.unscoped.where(account_id: account.id).outgoing
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .count
+ end
+
+ def resolutions_count
+ scope.conversations
+ .resolved
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .count
+ end
+
+ def avg_first_response_time
+ scope.events
+ .where(name: 'first_response')
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .average(:value)
+ end
+
+ def avg_resolution_time
+ scope.events.where(name: 'conversation_resolved')
+ .group_by_day(:created_at, range: range, default_value: 0)
+ .average(:value)
+ end
+
+ def range
+ parse_date_time(params[:since])..parse_date_time(params[:until])
+ end
+
+ # Taking average of average is not too accurate
+ # https://en.wikipedia.org/wiki/Simpson's_paradox
+ # TODO: Will optimize this later
+ def avg_resolution_time_summary
+ return 0 if avg_resolution_time.values.empty?
+
+ (avg_resolution_time.values.sum / avg_resolution_time.values.length)
+ end
+
+ def avg_first_response_time_summary
+ return 0 if avg_first_response_time.values.empty?
+
+ (avg_first_response_time.values.sum / avg_first_response_time.values.length)
+ end
+
+ def parse_date_time(datetime)
+ return datetime if datetime.is_a?(DateTime)
+ return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
+
+ DateTime.strptime(datetime, '%s')
+ end
+end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 097a40dab..8db37ced7 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -1,9 +1,16 @@
class Api::BaseController < ApplicationController
+ include AccessTokenAuthHelper
respond_to :json
- before_action :authenticate_user!
+ before_action :authenticate_access_token!, if: :authenticate_by_access_token?
+ before_action :validate_bot_access_token!, if: :authenticate_by_access_token?
+ before_action :authenticate_user!, unless: :authenticate_by_access_token?
private
+ def authenticate_by_access_token?
+ request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
+ end
+
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts/accounts_controller.rb
new file mode 100644
index 000000000..0fd5dc7cf
--- /dev/null
+++ b/app/controllers/api/v1/accounts/accounts_controller.rb
@@ -0,0 +1,54 @@
+class Api::V1::Accounts::AccountsController < Api::BaseController
+ include AuthHelper
+
+ skip_before_action :verify_authenticity_token, only: [:create]
+ skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
+ only: [:create], raise: false
+ before_action :check_signup_enabled, only: [:create]
+ before_action :check_authorization, except: [:create]
+ before_action :fetch_account, except: [:create]
+
+ rescue_from CustomExceptions::Account::InvalidEmail,
+ CustomExceptions::Account::UserExists,
+ CustomExceptions::Account::UserErrors,
+ with: :render_error_response
+
+ def create
+ @user = AccountBuilder.new(
+ account_name: account_params[:account_name],
+ email: account_params[:email]
+ ).perform
+ if @user
+ send_auth_headers(@user)
+ render 'devise/auth.json', locals: { resource: @user }
+ else
+ render_error_response(CustomExceptions::Account::SignupFailed.new({}))
+ end
+ end
+
+ def show
+ render 'api/v1/accounts/show.json'
+ end
+
+ def update
+ @account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
+ end
+
+ private
+
+ def check_authorization
+ authorize(Account)
+ end
+
+ def fetch_account
+ @account = current_user.accounts.find(params[:id])
+ end
+
+ def account_params
+ params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled)
+ end
+
+ def check_signup_enabled
+ raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
+ end
+end
diff --git a/app/controllers/api/v1/actions/contact_merges_controller.rb b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
similarity index 88%
rename from app/controllers/api/v1/actions/contact_merges_controller.rb
rename to app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
index 2eead4869..1296b6a55 100644
--- a/app/controllers/api/v1/actions/contact_merges_controller.rb
+++ b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::Actions::ContactMergesController < Api::BaseController
+class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
before_action :set_base_contact, only: [:create]
before_action :set_mergee_contact, only: [:create]
diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb
new file mode 100644
index 000000000..ca796ceef
--- /dev/null
+++ b/app/controllers/api/v1/accounts/agents_controller.rb
@@ -0,0 +1,69 @@
+class Api::V1::Accounts::AgentsController < Api::BaseController
+ before_action :fetch_agent, except: [:create, :index]
+ before_action :check_authorization
+ before_action :find_user, only: [:create]
+ before_action :create_user, only: [:create]
+ before_action :save_account_user, only: [:create]
+
+ def index
+ @agents = agents
+ end
+
+ def destroy
+ @agent.account_user.destroy
+ head :ok
+ end
+
+ def update
+ @agent.update!(agent_params.except(:role))
+ @agent.account_user.update!(role: agent_params[:role]) if agent_params[:role]
+ render 'api/v1/models/user.json', locals: { resource: @agent }
+ end
+
+ def create
+ render 'api/v1/models/user.json', locals: { resource: @user }
+ end
+
+ private
+
+ def check_authorization
+ authorize(User)
+ end
+
+ def fetch_agent
+ @agent = agents.find(params[:id])
+ end
+
+ def find_user
+ @user = User.find_by(email: new_agent_params[:email])
+ end
+
+ def create_user
+ return if @user
+
+ @user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation))
+ end
+
+ def save_account_user
+ AccountUser.create!(
+ account_id: current_account.id,
+ user_id: @user.id,
+ role: new_agent_params[:role],
+ inviter_id: current_user.id
+ )
+ end
+
+ def agent_params
+ params.require(:agent).permit(:email, :name, :role)
+ end
+
+ def new_agent_params
+ time = Time.now.to_i
+ params.require(:agent).permit(:email, :name, :role)
+ .merge!(password: time, password_confirmation: time, inviter: current_user)
+ end
+
+ def agents
+ @agents ||= current_account.users
+ end
+end
diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb
new file mode 100644
index 000000000..bf42b215a
--- /dev/null
+++ b/app/controllers/api/v1/accounts/callbacks_controller.rb
@@ -0,0 +1,105 @@
+class Api::V1::Accounts::CallbacksController < Api::BaseController
+ before_action :inbox, only: [:reauthorize_page]
+
+ def register_facebook_page
+ user_access_token = params[:user_access_token]
+ page_access_token = params[:page_access_token]
+ page_id = params[:page_id]
+ inbox_name = params[:inbox_name]
+ ActiveRecord::Base.transaction do
+ facebook_channel = current_account.facebook_pages.create!(
+ page_id: page_id, user_access_token: user_access_token,
+ page_access_token: page_access_token
+ )
+ @facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
+ set_avatar(@facebook_inbox, page_id)
+ rescue StandardError => e
+ Rails.logger e
+ end
+ end
+
+ def facebook_pages
+ @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
+ end
+
+ # get params[:inbox_id], current_account, params[:omniauth_token]
+ def reauthorize_page
+ if @inbox&.facebook?
+ fb_page_id = @inbox.channel.page_id
+ page_details = fb_object.get_connections('me', 'accounts')
+
+ if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
+ update_fb_page(fb_page_id, page_detail['access_token'])
+ return head :ok
+ end
+ end
+
+ head :unprocessable_entity
+ end
+
+ private
+
+ def inbox
+ @inbox = current_account.inboxes.find_by(id: params[:inbox_id])
+ end
+
+ def update_fb_page(fb_page_id, access_token)
+ get_fb_page(fb_page_id)&.update!(
+ user_access_token: @user_access_token, page_access_token: access_token
+ )
+ end
+
+ def get_fb_page(fb_page_id)
+ current_account.facebook_pages.find_by(page_id: fb_page_id)
+ end
+
+ def fb_object
+ @user_access_token = long_lived_token(params[:omniauth_token])
+ Koala::Facebook::API.new(@user_access_token)
+ end
+
+ def long_lived_token(omniauth_token)
+ koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
+ koala.exchange_access_token_info(omniauth_token)['access_token']
+ rescue StandardError => e
+ Rails.logger e
+ end
+
+ def mark_already_existing_facebook_pages(data)
+ return [] if data.empty?
+
+ data.inject([]) do |result, page_detail|
+ page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
+ result << page_detail
+ end
+ end
+
+ def set_avatar(facebook_inbox, page_id)
+ uri = get_avatar_url(page_id)
+
+ return unless uri
+
+ avatar_resource = LocalResource.new(uri)
+ facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
+ end
+
+ def get_avatar_url(page_id)
+ begin
+ url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
+ uri = URI.parse(url)
+ tries = 3
+ begin
+ response = uri.open(redirect: false)
+ rescue OpenURI::HTTPRedirect => e
+ uri = e.uri # assigned from the "Location" response header
+ retry if (tries -= 1).positive?
+ raise
+ end
+ pic_url = response.base_uri.to_s
+ rescue StandardError => e
+ Rails.logger.debug "Rescued: #{e.inspect}"
+ pic_url = nil
+ end
+ pic_url
+ end
+end
diff --git a/app/controllers/api/v1/canned_responses_controller.rb b/app/controllers/api/v1/accounts/canned_responses_controller.rb
similarity index 92%
rename from app/controllers/api/v1/canned_responses_controller.rb
rename to app/controllers/api/v1/accounts/canned_responses_controller.rb
index aa82ea3c4..b76da5f8c 100644
--- a/app/controllers/api/v1/canned_responses_controller.rb
+++ b/app/controllers/api/v1/accounts/canned_responses_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::CannedResponsesController < Api::BaseController
+class Api::V1::Accounts::CannedResponsesController < Api::BaseController
before_action :fetch_canned_response, only: [:update, :destroy]
def index
diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb
new file mode 100644
index 000000000..c0c121900
--- /dev/null
+++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb
@@ -0,0 +1,57 @@
+class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
+ before_action :authorize_request
+
+ def create
+ ActiveRecord::Base.transaction do
+ authenticate_twilio
+ build_inbox
+ setup_webhooks if @twilio_channel.sms?
+ rescue Twilio::REST::TwilioError => e
+ render_could_not_create_error(e.message)
+ rescue StandardError => e
+ render_could_not_create_error(e.message)
+ end
+ end
+
+ private
+
+ def authorize_request
+ authorize ::Inbox
+ end
+
+ def authenticate_twilio
+ client = Twilio::REST::Client.new(permitted_params[:account_sid], permitted_params[:auth_token])
+ client.messages.list(limit: 1)
+ end
+
+ def setup_webhooks
+ ::Twilio::WebhookSetupService.new(inbox: @inbox).perform
+ end
+
+ def phone_number
+ medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
+ end
+
+ def medium
+ permitted_params[:medium]
+ end
+
+ def build_inbox
+ @twilio_channel = current_account.twilio_sms.create!(
+ account_sid: permitted_params[:account_sid],
+ auth_token: permitted_params[:auth_token],
+ phone_number: phone_number,
+ medium: medium
+ )
+ @inbox = current_account.inboxes.create(
+ name: permitted_params[:name],
+ channel: @twilio_channel
+ )
+ end
+
+ def permitted_params
+ params.require(:twilio_channel).permit(
+ :account_id, :phone_number, :account_sid, :auth_token, :name, :medium
+ )
+ end
+end
diff --git a/app/controllers/api/v1/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb
similarity index 85%
rename from app/controllers/api/v1/contacts/conversations_controller.rb
rename to app/controllers/api/v1/accounts/contacts/conversations_controller.rb
index bce503ad6..8fcb4df13 100644
--- a/app/controllers/api/v1/contacts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::Contacts::ConversationsController < Api::BaseController
+class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
def index
@conversations = current_account.conversations.includes(
:assignee, :contact, :inbox
diff --git a/app/controllers/api/v1/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb
similarity index 93%
rename from app/controllers/api/v1/contacts_controller.rb
rename to app/controllers/api/v1/accounts/contacts_controller.rb
index b5885d708..9d95f69aa 100644
--- a/app/controllers/api/v1/contacts_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::ContactsController < Api::BaseController
+class Api::V1::Accounts::ContactsController < Api::BaseController
protect_from_forgery with: :null_session
before_action :check_authorization
diff --git a/app/controllers/api/v1/conversations/assignments_controller.rb b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb
similarity index 69%
rename from app/controllers/api/v1/conversations/assignments_controller.rb
rename to app/controllers/api/v1/accounts/conversations/assignments_controller.rb
index e411022fe..6da3c05da 100644
--- a/app/controllers/api/v1/conversations/assignments_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb
@@ -1,7 +1,8 @@
-class Api::V1::Conversations::AssignmentsController < Api::BaseController
+class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController
before_action :set_conversation, only: [:create]
- def create # assign agent to a conversation
+ # assign agent to a conversation
+ def create
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
assignee = current_account.users.find_by(id: params[:assignee_id])
@conversation.update_assignee(assignee)
diff --git a/app/controllers/api/v1/conversations/labels_controller.rb b/app/controllers/api/v1/accounts/conversations/labels_controller.rb
similarity index 61%
rename from app/controllers/api/v1/conversations/labels_controller.rb
rename to app/controllers/api/v1/accounts/conversations/labels_controller.rb
index e9074ac03..3e80e2825 100644
--- a/app/controllers/api/v1/conversations/labels_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations/labels_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::Conversations::LabelsController < Api::BaseController
+class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController
before_action :set_conversation, only: [:create, :index]
def create
@@ -6,7 +6,8 @@ class Api::V1::Conversations::LabelsController < Api::BaseController
@labels = @conversation.label_list
end
- def index # all labels of the current conversation
+ # all labels of the current conversation
+ def index
@labels = @conversation.label_list
end
end
diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
new file mode 100644
index 000000000..d0ed39edb
--- /dev/null
+++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
@@ -0,0 +1,18 @@
+class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController
+ before_action :set_conversation, only: [:index, :create]
+
+ def index
+ @messages = message_finder.perform
+ end
+
+ def create
+ mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
+ @message = mb.perform
+ end
+
+ private
+
+ def message_finder
+ @message_finder ||= MessageFinder.new(@conversation, params)
+ end
+end
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
similarity index 53%
rename from app/controllers/api/v1/conversations_controller.rb
rename to app/controllers/api/v1/accounts/conversations_controller.rb
index 6c44114be..3050c8ab7 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -1,5 +1,6 @@
-class Api::V1::ConversationsController < Api::BaseController
- before_action :set_conversation, except: [:index]
+class Api::V1::Accounts::ConversationsController < Api::BaseController
+ before_action :conversation, except: [:index]
+ before_action :contact_inbox, only: [:create]
def index
result = conversation_finder.perform
@@ -7,10 +8,12 @@ class Api::V1::ConversationsController < Api::BaseController
@conversations_count = result[:count]
end
- def show
- @messages = messages_finder.perform
+ def create
+ @conversation = ::Conversation.create!(conversation_params)
end
+ def show; end
+
def toggle_status
@status = @conversation.toggle_status
end
@@ -27,15 +30,24 @@ class Api::V1::ConversationsController < Api::BaseController
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
end
- def set_conversation
+ def conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
end
+ def contact_inbox
+ @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
+ end
+
+ def conversation_params
+ {
+ account_id: current_account.id,
+ inbox_id: @contact_inbox.inbox_id,
+ contact_id: @contact_inbox.contact_id,
+ contact_inbox_id: @contact_inbox.id
+ }
+ end
+
def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
-
- def messages_finder
- @message_finder ||= MessageFinder.new(@conversation, params)
- end
end
diff --git a/app/controllers/api/v1/facebook_indicators_controller.rb b/app/controllers/api/v1/accounts/facebook_indicators_controller.rb
similarity index 89%
rename from app/controllers/api/v1/facebook_indicators_controller.rb
rename to app/controllers/api/v1/accounts/facebook_indicators_controller.rb
index dccf508c9..7cea774cf 100644
--- a/app/controllers/api/v1/facebook_indicators_controller.rb
+++ b/app/controllers/api/v1/accounts/facebook_indicators_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::FacebookIndicatorsController < Api::BaseController
+class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
before_action :set_access_token
around_action :handle_with_exception
@@ -26,6 +26,7 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
def handle_with_exception
yield
rescue Facebook::Messenger::Error => e
+ Rails.logger.debug "Rescued: #{e.inspect}"
true
end
diff --git a/app/controllers/api/v1/inbox_members_controller.rb b/app/controllers/api/v1/accounts/inbox_members_controller.rb
similarity index 94%
rename from app/controllers/api/v1/inbox_members_controller.rb
rename to app/controllers/api/v1/accounts/inbox_members_controller.rb
index 982ad00ba..f71b3869d 100644
--- a/app/controllers/api/v1/inbox_members_controller.rb
+++ b/app/controllers/api/v1/accounts/inbox_members_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::InboxMembersController < Api::BaseController
+class Api::V1::Accounts::InboxMembersController < Api::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb
new file mode 100644
index 000000000..4b3ed836e
--- /dev/null
+++ b/app/controllers/api/v1/accounts/inboxes_controller.rb
@@ -0,0 +1,66 @@
+class Api::V1::Accounts::InboxesController < Api::BaseController
+ before_action :check_authorization
+ before_action :fetch_inbox, except: [:index, :create]
+ before_action :fetch_agent_bot, only: [:set_agent_bot]
+
+ def index
+ @inboxes = policy_scope(current_account.inboxes)
+ end
+
+ def create
+ ActiveRecord::Base.transaction do
+ channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
+ @inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
+ @inbox.avatar.attach(permitted_params[:avatar])
+ @inbox.save!
+ end
+ end
+
+ def update
+ @inbox.update(inbox_update_params.except(:channel))
+ @inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
+ end
+
+ def set_agent_bot
+ if @agent_bot
+ agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
+ agent_bot_inbox.agent_bot = @agent_bot
+ agent_bot_inbox.save!
+ elsif @inbox.agent_bot_inbox.present?
+ @inbox.agent_bot_inbox.destroy!
+ end
+ head :ok
+ end
+
+ def destroy
+ @inbox.destroy
+ head :ok
+ end
+
+ private
+
+ def fetch_inbox
+ @inbox = current_account.inboxes.find(params[:id])
+ end
+
+ def fetch_agent_bot
+ @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
+ end
+
+ def web_widgets
+ current_account.web_widgets
+ end
+
+ def check_authorization
+ authorize(Inbox)
+ end
+
+ def permitted_params
+ params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
+ end
+
+ def inbox_update_params
+ params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
+ :welcome_tagline, :agent_away_message])
+ end
+end
diff --git a/app/controllers/api/v1/labels_controller.rb b/app/controllers/api/v1/accounts/labels_controller.rb
similarity index 58%
rename from app/controllers/api/v1/labels_controller.rb
rename to app/controllers/api/v1/accounts/labels_controller.rb
index 4426b7018..c9f15bdae 100644
--- a/app/controllers/api/v1/labels_controller.rb
+++ b/app/controllers/api/v1/accounts/labels_controller.rb
@@ -1,5 +1,6 @@
-class Api::V1::LabelsController < Api::BaseController
- def index # list all labels in account
+class Api::V1::Accounts::LabelsController < Api::BaseController
+ # list all labels in account
+ def index
@labels = current_account.all_conversation_tags
end
diff --git a/app/controllers/api/v1/accounts/notification_settings_controller.rb b/app/controllers/api/v1/accounts/notification_settings_controller.rb
new file mode 100644
index 000000000..ba6e43804
--- /dev/null
+++ b/app/controllers/api/v1/accounts/notification_settings_controller.rb
@@ -0,0 +1,29 @@
+class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
+ before_action :set_user, :load_notification_setting
+
+ def show; end
+
+ def update
+ update_flags
+ @notification_setting.save!
+ render action: 'show'
+ end
+
+ private
+
+ def set_user
+ @user = current_user
+ end
+
+ def load_notification_setting
+ @notification_setting = @user.notification_settings.find_by(account_id: current_account.id)
+ end
+
+ def notification_setting_params
+ params.require(:notification_settings).permit(selected_email_flags: [])
+ end
+
+ def update_flags
+ @notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
+ end
+end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/accounts/reports_controller.rb
similarity index 82%
rename from app/controllers/api/v1/reports_controller.rb
rename to app/controllers/api/v1/accounts/reports_controller.rb
index e155d3af7..c93574b6c 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/accounts/reports_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::ReportsController < Api::BaseController
+class Api::V1::Accounts::ReportsController < Api::BaseController
include CustomExceptions::Report
include Constants::Report
@@ -36,10 +36,6 @@ class Api::V1::ReportsController < Api::BaseController
current_user.account
end
- def agent
- @agent ||= current_account.users.find(params[:agent_id])
- end
-
def account_summary_metrics
summary_metrics(ACCOUNT_METRICS, :account_summary_params, AVG_ACCOUNT_METRICS)
end
@@ -51,18 +47,18 @@ class Api::V1::ReportsController < Api::BaseController
def summary_metrics(metrics, calc_function, avg_metrics)
metrics.each_with_object({}) do |metric, result|
data = ReportBuilder.new(current_account, send(calc_function, metric)).build
-
- if avg_metrics.include?(metric)
- sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
- sum /= data.length unless sum.zero?
- else
- sum = data.inject(0) { |sum, hash| sum + hash[:value].to_i }
- end
-
- result[metric] = sum
+ result[metric] = calculate_metric(data, metric, avg_metrics)
end
end
+ def calculate_metric(data, metric, avg_metrics)
+ sum = data.inject(0) { |val, hash| val + hash[:value].to_i }
+ if avg_metrics.include?(metric)
+ sum /= data.length unless sum.zero?
+ end
+ sum
+ end
+
def account_summary_params(metric)
{
metric: metric.to_s,
diff --git a/app/controllers/api/v1/subscriptions_controller.rb b/app/controllers/api/v1/accounts/subscriptions_controller.rb
similarity index 76%
rename from app/controllers/api/v1/subscriptions_controller.rb
rename to app/controllers/api/v1/accounts/subscriptions_controller.rb
index 92e4f7f13..f9b3141d6 100644
--- a/app/controllers/api/v1/subscriptions_controller.rb
+++ b/app/controllers/api/v1/accounts/subscriptions_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::SubscriptionsController < Api::BaseController
+class Api::V1::Accounts::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
diff --git a/app/controllers/api/v1/inbox/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb
similarity index 77%
rename from app/controllers/api/v1/inbox/webhooks_controller.rb
rename to app/controllers/api/v1/accounts/webhooks_controller.rb
index 47ada02a8..dbdd953ed 100644
--- a/app/controllers/api/v1/inbox/webhooks_controller.rb
+++ b/app/controllers/api/v1/accounts/webhooks_controller.rb
@@ -1,4 +1,4 @@
-class Api::V1::Inbox::WebhooksController < Api::BaseController
+class Api::V1::Accounts::WebhooksController < Api::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]
@@ -23,7 +23,7 @@ class Api::V1::Inbox::WebhooksController < Api::BaseController
private
def webhook_params
- params.require(:webhook).permit(:account_id, :inbox_id, :urls).merge(urls: params[:urls])
+ params.require(:webhook).permit(:inbox_id, :url)
end
def fetch_webhook
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
deleted file mode 100644
index 0bbf2f67f..000000000
--- a/app/controllers/api/v1/accounts_controller.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-class Api::V1::AccountsController < Api::BaseController
- include AuthHelper
-
- skip_before_action :verify_authenticity_token, only: [:create]
- skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
- only: [:create], raise: false
-
- rescue_from CustomExceptions::Account::InvalidEmail,
- CustomExceptions::Account::UserExists,
- CustomExceptions::Account::UserErrors,
- with: :render_error_response
-
- def create
- @user = AccountBuilder.new(
- account_name: account_params[:account_name],
- email: account_params[:email]
- ).perform
- if @user
- send_auth_headers(@user)
- render json: {
- data: @user.token_validation_response
- }
- else
- render_error_response(CustomExceptions::Account::SignupFailed.new({}))
- end
- end
-
- private
-
- def account_params
- params.permit(:account_name, :email)
- end
-end
diff --git a/app/controllers/api/v1/agent_bots_controller.rb b/app/controllers/api/v1/agent_bots_controller.rb
new file mode 100644
index 000000000..4c17fd4f4
--- /dev/null
+++ b/app/controllers/api/v1/agent_bots_controller.rb
@@ -0,0 +1,8 @@
+class Api::V1::AgentBotsController < Api::BaseController
+ skip_before_action :authenticate_user!
+ skip_before_action :check_subscription
+
+ def index
+ render json: AgentBot.all
+ end
+end
diff --git a/app/controllers/api/v1/agents_controller.rb b/app/controllers/api/v1/agents_controller.rb
deleted file mode 100644
index a1758b40b..000000000
--- a/app/controllers/api/v1/agents_controller.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-class Api::V1::AgentsController < Api::BaseController
- before_action :fetch_agent, except: [:create, :index]
- before_action :check_authorization
- before_action :build_agent, only: [:create]
-
- def index
- @agents = agents
- end
-
- def destroy
- @agent.destroy
- head :ok
- end
-
- def update
- @agent.update!(agent_params)
- render json: @agent
- end
-
- def create
- @agent.save!
- render json: @agent
- end
-
- private
-
- def check_authorization
- authorize(User)
- end
-
- def fetch_agent
- @agent = agents.find(params[:id])
- end
-
- def build_agent
- @agent = agents.new(new_agent_params)
- end
-
- def agent_params
- params.require(:agent).permit(:email, :name, :role)
- end
-
- def new_agent_params
- time = Time.now.to_i
- params.require(:agent).permit(:email, :name, :role)
- .merge!(password: time, password_confirmation: time, inviter: current_user)
- end
-
- def agents
- @agents ||= current_account.users
- end
-end
diff --git a/app/controllers/api/v1/callbacks_controller.rb b/app/controllers/api/v1/callbacks_controller.rb
deleted file mode 100644
index cc6b00958..000000000
--- a/app/controllers/api/v1/callbacks_controller.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'rest-client'
-require 'telegram/bot'
-class Api::V1::CallbacksController < ApplicationController
- skip_before_action :verify_authenticity_token, only: [:register_facebook_page]
- skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
-
- def register_facebook_page
- user_access_token = params[:user_access_token]
- page_access_token = params[:page_access_token]
- page_name = params[:page_name]
- page_id = params[:page_id]
- inbox_name = params[:inbox_name]
- facebook_channel = current_account.facebook_pages.create!(
- name: page_name, page_id: page_id, user_access_token: user_access_token,
- page_access_token: page_access_token
- )
- set_avatar(facebook_channel, page_id)
- inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
- render json: inbox
- end
-
- def get_facebook_pages
- @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
- end
-
- # get params[:inbox_id], current_account, params[:omniauth_token]
- def reauthorize_page
- if @inbox&.first&.facebook?
- fb_page_id = @inbox.channel.page_id
- page_details = fb_object.get_connections('me', 'accounts')
-
- (page_details || []).each do |page_detail|
- if fb_page_id == page_detail['id'] # found the page which has to be reauthorised
- update_fb_page(fb_page_id, page_detail['access_token'])
- head :ok
- end
- end
- end
-
- head :unprocessable_entity
- end
-
- private
-
- def inbox
- @inbox = current_account.inboxes.find_by(id: params[:inbox_id])
- end
-
- def update_fb_page
- if fb_page(fb_page_id)
- fb_page.update_attributes!(
- user_access_token: @user_access_token, page_access_token: access_token
- )
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def fb_page(fb_page_id)
- current_account.facebook_pages.find_by(page_id: fb_page_id)
- end
-
- def fb_object
- @user_access_token = long_lived_token(params[:omniauth_token])
- Koala::Facebook::API.new(@user_access_token)
- end
-
- def long_lived_token(omniauth_token)
- koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
- long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token']
- end
-
- def mark_already_existing_facebook_pages(data)
- return [] if data.empty?
-
- data.inject([]) do |result, page_detail|
- current_account.facebook_pages.exists?(page_id: page_detail['id']) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
- result << page_detail
- end
- end
-
- def set_avatar(facebook_channel, page_id)
- avatar_resource = LocalResource.new(get_avatar_url(page_id))
- facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
- end
-
- def get_avatar_url(page_id)
- begin
- url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
- uri = URI.parse(url)
- tries = 3
- begin
- response = uri.open(redirect: false)
- rescue OpenURI::HTTPRedirect => e
- uri = e.uri # assigned from the "Location" response header
- retry if (tries -= 1) > 0
- raise
- end
- pic_url = response.base_uri.to_s
- Rails.logger.info(pic_url)
- rescue StandardError => e
- pic_url = nil
- end
- pic_url
- end
-end
diff --git a/app/controllers/api/v1/conversations/messages_controller.rb b/app/controllers/api/v1/conversations/messages_controller.rb
deleted file mode 100644
index 22fdd88fb..000000000
--- a/app/controllers/api/v1/conversations/messages_controller.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class Api::V1::Conversations::MessagesController < Api::BaseController
- before_action :set_conversation, only: [:create]
-
- def create
- mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
- @message = mb.perform
- end
-end
diff --git a/app/controllers/api/v1/inboxes_controller.rb b/app/controllers/api/v1/inboxes_controller.rb
deleted file mode 100644
index e9005a0c7..000000000
--- a/app/controllers/api/v1/inboxes_controller.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-class Api::V1::InboxesController < Api::BaseController
- before_action :check_authorization
- before_action :fetch_inbox, only: [:destroy, :update]
-
- def index
- @inboxes = policy_scope(current_account.inboxes)
- end
-
- def destroy
- @inbox.destroy
- head :ok
- end
-
- def update
- @inbox.update(inbox_update_params)
- end
-
- private
-
- def fetch_inbox
- @inbox = current_account.inboxes.find(params[:id])
- end
-
- def check_authorization
- authorize(Inbox)
- end
-
- def inbox_update_params
- params.require(:inbox).permit(:enable_auto_assignment)
- end
-end
diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb
index d15b414c1..5db576000 100644
--- a/app/controllers/api/v1/webhooks_controller.rb
+++ b/app/controllers/api/v1/webhooks_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::WebhooksController < ApplicationController
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
+
def chargebee
chargebee_consumer.consume
head :ok
diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb
index eb19e2bdd..9ef4a04f2 100644
--- a/app/controllers/api/v1/widget/base_controller.rb
+++ b/app/controllers/api/v1/widget/base_controller.rb
@@ -2,9 +2,9 @@ class Api::V1::Widget::BaseController < ApplicationController
private
def conversation
- @conversation ||= @contact_inbox.conversations.find_by(
+ @conversation ||= @contact_inbox.conversations.where(
inbox_id: auth_token_params[:inbox_id]
- )
+ ).last
end
def auth_token_params
diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb
new file mode 100644
index 000000000..b7ac793e7
--- /dev/null
+++ b/app/controllers/api/v1/widget/contacts_controller.rb
@@ -0,0 +1,18 @@
+class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
+ before_action :set_web_widget
+ before_action :set_contact
+
+ def update
+ contact_identify_action = ContactIdentifyAction.new(
+ contact: @contact,
+ params: permitted_params.to_h.deep_symbolize_keys
+ )
+ render json: contact_identify_action.perform
+ end
+
+ private
+
+ def permitted_params
+ params.permit(:website_token, :identifier, :email, :name, :avatar_url)
+ end
+end
diff --git a/app/controllers/api/v1/widget/events_controller.rb b/app/controllers/api/v1/widget/events_controller.rb
new file mode 100644
index 000000000..faa44994c
--- /dev/null
+++ b/app/controllers/api/v1/widget/events_controller.rb
@@ -0,0 +1,16 @@
+class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
+ include Events::Types
+ before_action :set_web_widget
+ before_action :set_contact
+
+ def create
+ Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox)
+ head :no_content
+ end
+
+ private
+
+ def permitted_params
+ params.permit(:name, :website_token)
+ end
+end
diff --git a/app/controllers/api/v1/widget/inboxes_controller.rb b/app/controllers/api/v1/widget/inboxes_controller.rb
deleted file mode 100644
index ce739fef0..000000000
--- a/app/controllers/api/v1/widget/inboxes_controller.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-class Api::V1::Widget::InboxesController < Api::BaseController
- before_action :authorize_request
- before_action :set_web_widget_channel, only: [:update]
- before_action :set_inbox, only: [:update]
-
- def create
- ActiveRecord::Base.transaction do
- channel = web_widgets.create!(
- website_name: permitted_params[:website][:website_name],
- website_url: permitted_params[:website][:website_url],
- widget_color: permitted_params[:website][:widget_color]
- )
- @inbox = inboxes.create!(name: permitted_params[:website][:website_name], channel: channel)
- end
- end
-
- def update
- @channel.update!(
- widget_color: permitted_params[:website][:widget_color]
- )
- end
-
- private
-
- def authorize_request
- authorize ::Inbox
- end
-
- def inboxes
- current_account.inboxes
- end
-
- def web_widgets
- current_account.web_widgets
- end
-
- def set_web_widget_channel
- @channel = web_widgets.find_by(id: permitted_params[:id])
- end
-
- def set_inbox
- @inbox = @channel.inbox
- end
-
- def permitted_params
- params.permit(:id, website: [:website_name, :website_url, :widget_color])
- end
-end
diff --git a/app/controllers/api/v1/widget/labels_controller.rb b/app/controllers/api/v1/widget/labels_controller.rb
new file mode 100644
index 000000000..efe84f5e3
--- /dev/null
+++ b/app/controllers/api/v1/widget/labels_controller.rb
@@ -0,0 +1,24 @@
+class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
+ before_action :set_web_widget
+ before_action :set_contact
+
+ def create
+ conversation.label_list.add(permitted_params[:label])
+ conversation.save!
+
+ head :no_content
+ end
+
+ def destroy
+ conversation.label_list.remove(permitted_params[:id])
+ conversation.save!
+
+ head :no_content
+ end
+
+ private
+
+ def permitted_params
+ params.permit(:id, :label, :website_token)
+ end
+end
diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb
index a6087bb8c..a65e01f9c 100644
--- a/app/controllers/api/v1/widget/messages_controller.rb
+++ b/app/controllers/api/v1/widget/messages_controller.rb
@@ -10,20 +10,36 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def create
@message = conversation.messages.new(message_params)
- @message.save!
- render json: @message
+ @message.save
+ build_attachment
end
def update
- @message.update!(input_submitted_email: contact_email)
- update_contact(contact_email)
- head :no_content
+ if @message.content_type == 'input_email'
+ @message.update!(submitted_email: contact_email)
+ update_contact(contact_email)
+ else
+ @message.update!(message_update_params[:message])
+ end
rescue StandardError => e
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
end
private
+ def build_attachment
+ return if params[:message][:attachments].blank?
+
+ params[:message][:attachments].each do |uploaded_attachment|
+ attachment = @message.attachments.new(
+ account_id: @message.account_id,
+ file_type: helpers.file_type(uploaded_attachment&.content_type)
+ )
+ attachment.file.attach(uploaded_attachment)
+ end
+ @message.save!
+ end
+
def set_conversation
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
end
@@ -31,9 +47,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params
{
account_id: conversation.account_id,
+ contact_id: @contact.id,
+ content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
- message_type: :incoming,
- content: permitted_params[:message][:content]
+ message_type: :incoming
}
end
@@ -85,7 +102,11 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def update_contact(email)
contact_with_email = @account.contacts.find_by(email: email)
if contact_with_email
- ::ContactMergeAction.new(account: @account, base_contact: contact_with_email, mergee_contact: @contact).perform
+ @contact = ::ContactMergeAction.new(
+ account: @account,
+ base_contact: contact_with_email,
+ mergee_contact: @contact
+ ).perform
else
@contact.update!(
email: email,
@@ -102,6 +123,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
contact_email.split('@')[0]
end
+ def message_update_params
+ params.permit(message: [submitted_values: [:name, :title, :value]])
+ end
+
def permitted_params
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
end
diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb
new file mode 100644
index 000000000..fe94db4e1
--- /dev/null
+++ b/app/controllers/api/v2/accounts/reports_controller.rb
@@ -0,0 +1,39 @@
+class Api::V2::Accounts::ReportsController < Api::BaseController
+ def account
+ builder = V2::ReportBuilder.new(current_account, account_report_params)
+ data = builder.build
+ render json: data
+ end
+
+ def account_summary
+ render json: account_summary_metrics
+ end
+
+ private
+
+ def current_account
+ current_user.account
+ end
+
+ def account_summary_params
+ {
+ type: :account,
+ since: params[:since],
+ until: params[:until]
+ }
+ end
+
+ def account_report_params
+ {
+ metric: params[:metric],
+ type: :account,
+ since: params[:since],
+ until: params[:until]
+ }
+ end
+
+ def account_summary_metrics
+ builder = V2::ReportBuilder.new(current_account, account_summary_params)
+ builder.summary
+ end
+end
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
new file mode 100644
index 000000000..3d7f59f0c
--- /dev/null
+++ b/app/controllers/api_controller.rb
@@ -0,0 +1,8 @@
+class ApiController < ApplicationController
+ skip_before_action :set_current_user, only: [:index]
+ skip_before_action :check_subscription, only: [:index]
+
+ def index
+ render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f38c45c63..5bac8991e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -14,7 +14,25 @@ class ApplicationController < ActionController::Base
private
def current_account
- @_ ||= current_user.account
+ @_ ||= find_current_account
+ end
+
+ def find_current_account
+ account = Account.find(params[:account_id])
+ if current_user
+ account_accessible_for_user?(account)
+ elsif @resource&.is_a?(AgentBot)
+ account_accessible_for_bot?(account)
+ end
+ account
+ end
+
+ def account_accessible_for_user?(account)
+ render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
+ end
+
+ def account_accessible_for_bot?(account)
+ render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
def handle_with_exception
diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb
new file mode 100644
index 000000000..e7af9e116
--- /dev/null
+++ b/app/controllers/concerns/access_token_auth_helper.rb
@@ -0,0 +1,26 @@
+module AccessTokenAuthHelper
+ BOT_ACCESSIBLE_ENDPOINTS = {
+ 'api/v1/accounts/conversations' => %w[toggle_status create],
+ 'api/v1/accounts/conversations/messages' => ['create']
+ }.freeze
+
+ def authenticate_access_token!
+ token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
+ access_token = AccessToken.find_by(token: token)
+ render_unauthorized('Invalid Access Token') && return unless access_token
+
+ token_owner = access_token.owner
+ @resource = token_owner
+ end
+
+ def validate_bot_access_token!
+ return if current_user.is_a?(User)
+ return if agent_bot_accessible?
+
+ render_unauthorized('Access to this endpoint is not authorized for bots')
+ end
+
+ def agent_bot_accessible?
+ BOT_ACCESSIBLE_ENDPOINTS.fetch(params[:controller], []).include?(params[:action])
+ end
+end
diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb
index adea8687d..4289d5af2 100644
--- a/app/controllers/devise_overrides/passwords_controller.rb
+++ b/app/controllers/devise_overrides/passwords_controller.rb
@@ -11,9 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable)
- render json: {
- data: @recoverable.token_validation_response
- }
+ render 'devise/auth.json', locals: { resource: @recoverable }
else
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
end
diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb
index 3a6614074..b9cec5447 100644
--- a/app/controllers/devise_overrides/sessions_controller.rb
+++ b/app/controllers/devise_overrides/sessions_controller.rb
@@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
wrap_parameters format: []
def render_create_success
- render 'devise/auth.json'
+ render 'devise/auth.json', locals: { resource: @resource }
end
end
diff --git a/app/controllers/swagger_controller.rb b/app/controllers/swagger_controller.rb
new file mode 100644
index 000000000..c5f8c0f5b
--- /dev/null
+++ b/app/controllers/swagger_controller.rb
@@ -0,0 +1,18 @@
+class SwaggerController < ApplicationController
+ def respond
+ if Rails.env.development? || Rails.env.test?
+ render inline: File.read(Rails.root.join('swagger', derived_path))
+ else
+ head 404
+ end
+ end
+
+ private
+
+ def derived_path
+ params[:path] ||= 'index.html'
+ path = params[:path]
+ path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s)
+ path
+ end
+end
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
new file mode 100644
index 000000000..44dcc9b6f
--- /dev/null
+++ b/app/controllers/twilio/callback_controller.rb
@@ -0,0 +1,31 @@
+class Twilio::CallbackController < ApplicationController
+ def create
+ ::Twilio::IncomingMessageService.new(params: permitted_params).perform
+
+ head :no_content
+ end
+
+ private
+
+ def permitted_params
+ params.permit(
+ :ApiVersion,
+ :SmsSid,
+ :From,
+ :ToState,
+ :ToZip,
+ :AccountSid,
+ :MessageSid,
+ :FromCountry,
+ :ToCity,
+ :FromCity,
+ :To,
+ :FromZip,
+ :Body,
+ :ToCountry,
+ :FromState,
+ :MediaUrl0,
+ :MediaContentType0
+ )
+ end
+end
diff --git a/app/controllers/twitter/authorizations_controller.rb b/app/controllers/twitter/authorizations_controller.rb
index 69145ff84..765e026fe 100644
--- a/app/controllers/twitter/authorizations_controller.rb
+++ b/app/controllers/twitter/authorizations_controller.rb
@@ -6,7 +6,7 @@ class Twitter::AuthorizationsController < Twitter::BaseController
::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token)
else
- redirect_to app_new_twitter_inbox_url
+ redirect_to app_new_twitter_inbox_url(account_id: account.id)
end
end
diff --git a/app/controllers/twitter/callbacks_controller.rb b/app/controllers/twitter/callbacks_controller.rb
index 876720f26..6065e3546 100644
--- a/app/controllers/twitter/callbacks_controller.rb
+++ b/app/controllers/twitter/callbacks_controller.rb
@@ -1,5 +1,7 @@
class Twitter::CallbacksController < Twitter::BaseController
def show
+ return redirect_to twitter_app_redirect_url if permitted_params[:denied]
+
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
@@ -8,9 +10,9 @@ class Twitter::CallbacksController < Twitter::BaseController
inbox = build_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
- redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
+ redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
else
- redirect_to app_new_twitter_inbox_url
+ redirect_to twitter_app_redirect_url
end
end
@@ -28,13 +30,16 @@ class Twitter::CallbacksController < Twitter::BaseController
@account ||= Account.find_by!(id: account_id)
end
+ def twitter_app_redirect_url
+ app_new_twitter_inbox_url(account_id: account.id)
+ end
+
def build_inbox
ActiveRecord::Base.transaction do
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
- profile_id: parsed_body['user_id'],
- name: parsed_body['screen_name']
+ profile_id: parsed_body['user_id']
)
account.inboxes.create(
name: parsed_body['screen_name'],
@@ -46,6 +51,6 @@ class Twitter::CallbacksController < Twitter::BaseController
end
def permitted_params
- params.permit(:oauth_token, :oauth_verifier)
+ params.permit(:oauth_token, :oauth_verifier, :denied)
end
end
diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb
index 97da6de93..72e44a1c2 100644
--- a/app/dispatchers/async_dispatcher.rb
+++ b/app/dispatchers/async_dispatcher.rb
@@ -1,11 +1,16 @@
class AsyncDispatcher < BaseDispatcher
def dispatch(event_name, timestamp, data)
+ EventDispatcherJob.perform_later(event_name, timestamp, data)
+ end
+
+ def publish_event(event_name, timestamp, data)
event_object = Events::Base.new(event_name, timestamp, data)
publish(event_object.method_name, event_object)
end
def listeners
- listeners = [ReportingListener.instance]
+ listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
+ listeners << EventListener.instance
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners
end
diff --git a/app/dispatchers/sync_dispatcher.rb b/app/dispatchers/sync_dispatcher.rb
index e3bad28a1..509a42727 100644
--- a/app/dispatchers/sync_dispatcher.rb
+++ b/app/dispatchers/sync_dispatcher.rb
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end
def listeners
- [ActionCableListener.instance]
+ [ActionCableListener.instance, AgentBotListener.instance]
end
end
diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb
index f4e114bf4..fcc0daf98 100644
--- a/app/finders/conversation_finder.rb
+++ b/app/finders/conversation_finder.rb
@@ -1,23 +1,18 @@
class ConversationFinder
attr_reader :current_user, :current_account, :params
- ASSIGNEE_TYPES = { me: 0, unassigned: 1, all: 2 }.freeze
-
- ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert
- ASSIGNEE_TYPES_BY_ID.default = :me
-
DEFAULT_STATUS = 'open'.freeze
# assumptions
# inbox_id if not given, take from all conversations, else specific to inbox
- # assignee_type if not given, take 'me'
+ # assignee_type if not given, take 'all'
# conversation_status if not given, take 'open'
# response of this class will be of type
# {conversations: [array of conversations], count: {open: count, resolved: count}}
# params
- # assignee_type_id, inbox_id, :status
+ # assignee_type, inbox_id, :status
def initialize(current_user, params)
@current_user = current_user
@@ -62,7 +57,7 @@ class ConversationFinder
end
def set_assignee_type
- @assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]]
+ @assignee_type = params[:assignee_type]
end
def find_all_conversations
@@ -72,12 +67,10 @@ class ConversationFinder
end
def filter_by_assignee_type
- if @assignee_type_id == ASSIGNEE_TYPES[:me]
+ if @assignee_type == 'me'
@conversations = @conversations.assigned_to(current_user)
- elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned]
+ elsif @assignee_type == 'unassigned'
@conversations = @conversations.unassigned
- elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
- @conversations
end
@conversations
end
diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb
index f21093b48..00ef8ab3b 100644
--- a/app/finders/message_finder.rb
+++ b/app/finders/message_finder.rb
@@ -11,7 +11,7 @@ class MessageFinder
private
def conversation_messages
- @conversation.messages.includes(:attachment, user: { avatar_attachment: :blob })
+ @conversation.messages.includes(:attachments, user: { avatar_attachment: :blob })
end
def messages
diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb
new file mode 100644
index 000000000..9f6939565
--- /dev/null
+++ b/app/helpers/file_type_helper.rb
@@ -0,0 +1,14 @@
+module FileTypeHelper
+ def file_type(content_type)
+ return :image if [
+ 'image/jpeg',
+ 'image/png',
+ 'image/svg+xml',
+ 'image/gif',
+ 'image/tiff',
+ 'image/bmp'
+ ].include?(content_type)
+
+ :file
+ end
+end
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index acf177d7c..bef1323e2 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -8,7 +8,10 @@
diff --git a/app/javascript/dashboard/api/ApiClient.js b/app/javascript/dashboard/api/ApiClient.js
index a3b87dae1..0b2ed28d0 100644
--- a/app/javascript/dashboard/api/ApiClient.js
+++ b/app/javascript/dashboard/api/ApiClient.js
@@ -3,9 +3,25 @@
const API_VERSION = `/api/v1`;
class ApiClient {
- constructor(url) {
+ constructor(resource, options = {}) {
this.apiVersion = API_VERSION;
- this.url = `${this.apiVersion}/${url}`;
+ this.options = options;
+ this.resource = resource;
+ }
+
+ get url() {
+ let url = this.apiVersion;
+ if (this.options.accountScoped) {
+ const isInsideAccountScopedURLs = window.location.pathname.includes(
+ '/app/accounts'
+ );
+
+ if (isInsideAccountScopedURLs) {
+ const accountId = window.location.pathname.split('/')[3];
+ url = `${url}/accounts/${accountId}`;
+ }
+ }
+ return `${url}/${this.resource}`;
}
get() {
diff --git a/app/javascript/dashboard/api/account.js b/app/javascript/dashboard/api/account.js
new file mode 100644
index 000000000..207420da6
--- /dev/null
+++ b/app/javascript/dashboard/api/account.js
@@ -0,0 +1,9 @@
+import ApiClient from './ApiClient';
+
+class AccountAPI extends ApiClient {
+ constructor() {
+ super('', { accountScoped: true });
+ }
+}
+
+export default new AccountAPI();
diff --git a/app/javascript/dashboard/api/agents.js b/app/javascript/dashboard/api/agents.js
index 62d8e6623..7cc5e6d0c 100644
--- a/app/javascript/dashboard/api/agents.js
+++ b/app/javascript/dashboard/api/agents.js
@@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Agents extends ApiClient {
constructor() {
- super('agents');
+ super('agents', { accountScoped: true });
}
}
diff --git a/app/javascript/dashboard/api/cannedResponse.js b/app/javascript/dashboard/api/cannedResponse.js
index 3a17d2735..f558dcaca 100644
--- a/app/javascript/dashboard/api/cannedResponse.js
+++ b/app/javascript/dashboard/api/cannedResponse.js
@@ -4,7 +4,7 @@ import ApiClient from './ApiClient';
class CannedResponse extends ApiClient {
constructor() {
- super('canned_responses');
+ super('canned_responses', { accountScoped: true });
}
get({ searchKey }) {
diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js
index f9781097c..e53885b4a 100644
--- a/app/javascript/dashboard/api/channel/fbChannel.js
+++ b/app/javascript/dashboard/api/channel/fbChannel.js
@@ -3,7 +3,7 @@ import ApiClient from '../ApiClient';
class FBChannel extends ApiClient {
constructor() {
- super('facebook_indicators');
+ super('facebook_indicators', { accountScoped: true });
}
markSeen({ inboxId, contactId }) {
@@ -22,7 +22,7 @@ class FBChannel extends ApiClient {
create(params) {
return axios.post(
- `${this.apiVersion}/callbacks/register_facebook_page`,
+ `${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,
params
);
}
diff --git a/app/javascript/dashboard/api/channel/twilioChannel.js b/app/javascript/dashboard/api/channel/twilioChannel.js
new file mode 100644
index 000000000..a688a1f11
--- /dev/null
+++ b/app/javascript/dashboard/api/channel/twilioChannel.js
@@ -0,0 +1,9 @@
+import ApiClient from '../ApiClient';
+
+class TwilioChannel extends ApiClient {
+ constructor() {
+ super('channels/twilio_channel', { accountScoped: true });
+ }
+}
+
+export default new TwilioChannel();
diff --git a/app/javascript/dashboard/api/channel/webChannel.js b/app/javascript/dashboard/api/channel/webChannel.js
index 7fc5fb2db..81a145462 100644
--- a/app/javascript/dashboard/api/channel/webChannel.js
+++ b/app/javascript/dashboard/api/channel/webChannel.js
@@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
class WebChannel extends ApiClient {
constructor() {
- super('widget/inboxes');
+ super('inboxes', { accountScoped: true });
}
}
diff --git a/app/javascript/dashboard/api/channels.js b/app/javascript/dashboard/api/channels.js
index f7db9afbc..25998b1a2 100644
--- a/app/javascript/dashboard/api/channels.js
+++ b/app/javascript/dashboard/api/channels.js
@@ -5,9 +5,9 @@
import endPoints from './endPoints';
export default {
- fetchFacebookPages(token) {
+ fetchFacebookPages(token, accountId) {
const urlData = endPoints('fetchFacebookPages');
urlData.params.omniauth_token = token;
- return axios.post(urlData.url, urlData.params);
+ return axios.post(urlData.url(accountId), urlData.params);
},
};
diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js
index bad89b182..0988141d3 100644
--- a/app/javascript/dashboard/api/contacts.js
+++ b/app/javascript/dashboard/api/contacts.js
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class ContactAPI extends ApiClient {
constructor() {
- super('contacts');
+ super('contacts', { accountScoped: true });
}
getConversations(contactId) {
diff --git a/app/javascript/dashboard/api/conversations.js b/app/javascript/dashboard/api/conversations.js
index fd36f8db3..876103694 100644
--- a/app/javascript/dashboard/api/conversations.js
+++ b/app/javascript/dashboard/api/conversations.js
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class ConversationApi extends ApiClient {
constructor() {
- super('conversations');
+ super('conversations', { accountScoped: true });
}
getLabels(conversationID) {
diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js
index 10a0608bd..53c669eb6 100644
--- a/app/javascript/dashboard/api/endPoints.js
+++ b/app/javascript/dashboard/api/endPoints.js
@@ -28,23 +28,12 @@ const endPoints = {
},
fetchFacebookPages: {
- url: 'api/v1/callbacks/get_facebook_pages.json',
+ url(accountId) {
+ return `api/v1/accounts/${accountId}/callbacks/facebook_pages.json`;
+ },
params: { omniauth_token: '' },
},
- reports: {
- account(metric, from, to) {
- return {
- url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`,
- };
- },
- accountSummary(accountId, from, to) {
- return {
- url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`,
- };
- },
- },
-
subscriptions: {
get() {
return {
diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js
index b444d0e17..d5212957a 100644
--- a/app/javascript/dashboard/api/inbox/conversation.js
+++ b/app/javascript/dashboard/api/inbox/conversation.js
@@ -3,15 +3,16 @@ import ApiClient from '../ApiClient';
class ConversationApi extends ApiClient {
constructor() {
- super('conversations');
+ super('conversations', { accountScoped: true });
}
- get({ inboxId, status, assigneeType }) {
+ get({ inboxId, status, assigneeType, page }) {
return axios.get(this.url, {
params: {
inbox_id: inboxId,
status,
- assignee_type_id: assigneeType,
+ assignee_type: assigneeType,
+ page,
},
});
}
diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js
index 367e3e00c..c9681f685 100644
--- a/app/javascript/dashboard/api/inbox/message.js
+++ b/app/javascript/dashboard/api/inbox/message.js
@@ -4,21 +4,31 @@ import ApiClient from '../ApiClient';
class MessageApi extends ApiClient {
constructor() {
- super('conversations');
+ super('conversations', { accountScoped: true });
}
create({ conversationId, message, private: isPrivate }) {
return axios.post(`${this.url}/${conversationId}/messages`, {
- message,
+ content: message,
private: isPrivate,
});
}
getPreviousMessages({ conversationId, before }) {
- return axios.get(`${this.url}/${conversationId}`, {
+ return axios.get(`${this.url}/${conversationId}/messages`, {
params: { before },
});
}
+
+ sendAttachment([conversationId, { file }]) {
+ const formData = new FormData();
+ formData.append('attachments[]', file, file.name);
+ return axios({
+ method: 'post',
+ url: `${this.url}/${conversationId}/messages`,
+ data: formData,
+ });
+ }
}
export default new MessageApi();
diff --git a/app/javascript/dashboard/api/inboxMembers.js b/app/javascript/dashboard/api/inboxMembers.js
index 2d7001562..3716f89ab 100644
--- a/app/javascript/dashboard/api/inboxMembers.js
+++ b/app/javascript/dashboard/api/inboxMembers.js
@@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
class InboxMembers extends ApiClient {
constructor() {
- super('inbox_members');
+ super('inbox_members', { accountScoped: true });
}
create({ inboxId, agentList }) {
diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js
index fb3e63dfd..b5cea1d01 100644
--- a/app/javascript/dashboard/api/inboxes.js
+++ b/app/javascript/dashboard/api/inboxes.js
@@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
class Inboxes extends ApiClient {
constructor() {
- super('inboxes');
+ super('inboxes', { accountScoped: true });
}
}
diff --git a/app/javascript/dashboard/api/reports.js b/app/javascript/dashboard/api/reports.js
index 94d6ac726..d2a96cda8 100644
--- a/app/javascript/dashboard/api/reports.js
+++ b/app/javascript/dashboard/api/reports.js
@@ -1,14 +1,22 @@
/* global axios */
+import ApiClient from './ApiClient';
-import endPoints from './endPoints';
+class ReportsAPI extends ApiClient {
+ constructor() {
+ super('reports', { accountScoped: true });
+ }
-export default {
- getAccountReports(metric, from, to) {
- const { url } = endPoints('reports').account(metric, from, to);
- return axios.get(url);
- },
- getAccountSummary(accountId, from, to) {
- const urlData = endPoints('reports').accountSummary(accountId, from, to);
- return axios.get(urlData.url);
- },
-};
+ getAccountReports(metric, since, until) {
+ return axios.get(`${this.url}/account`, {
+ params: { metric, since, until },
+ });
+ }
+
+ getAccountSummary(accountId, since, until) {
+ return axios.get(`${this.url}/${accountId}/account_summary`, {
+ params: { since, until },
+ });
+ }
+}
+
+export default new ReportsAPI();
diff --git a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
new file mode 100644
index 000000000..63ae1492d
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
@@ -0,0 +1,15 @@
+import fbChannel from '../../channel/fbChannel';
+import ApiClient from '../../ApiClient';
+
+describe('#FBChannel', () => {
+ it('creates correct instance', () => {
+ expect(fbChannel).toBeInstanceOf(ApiClient);
+ expect(fbChannel).toHaveProperty('get');
+ expect(fbChannel).toHaveProperty('show');
+ expect(fbChannel).toHaveProperty('create');
+ expect(fbChannel).toHaveProperty('update');
+ expect(fbChannel).toHaveProperty('delete');
+ expect(fbChannel).toHaveProperty('markSeen');
+ expect(fbChannel).toHaveProperty('toggleTyping');
+ });
+});
diff --git a/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js b/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js
new file mode 100644
index 000000000..f65f1c3a3
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/userNotificationSettings.spec.js
@@ -0,0 +1,13 @@
+import userNotificationSettings from '../userNotificationSettings';
+import ApiClient from '../ApiClient';
+
+describe('#AgentAPI', () => {
+ it('creates correct instance', () => {
+ expect(userNotificationSettings).toBeInstanceOf(ApiClient);
+ expect(userNotificationSettings).toHaveProperty('get');
+ expect(userNotificationSettings).toHaveProperty('show');
+ expect(userNotificationSettings).toHaveProperty('create');
+ expect(userNotificationSettings).toHaveProperty('update');
+ expect(userNotificationSettings).toHaveProperty('delete');
+ });
+});
diff --git a/app/javascript/dashboard/api/userNotificationSettings.js b/app/javascript/dashboard/api/userNotificationSettings.js
new file mode 100644
index 000000000..33829a6bd
--- /dev/null
+++ b/app/javascript/dashboard/api/userNotificationSettings.js
@@ -0,0 +1,14 @@
+/* global axios */
+import ApiClient from './ApiClient';
+
+class UserNotificationSettings extends ApiClient {
+ constructor() {
+ super('notification_settings', { accountScoped: true });
+ }
+
+ update(params) {
+ return axios.patch(`${this.url}`, params);
+ }
+}
+
+export default new UserNotificationSettings();
diff --git a/app/javascript/dashboard/api/webhooks.js b/app/javascript/dashboard/api/webhooks.js
new file mode 100644
index 000000000..1e03f25f7
--- /dev/null
+++ b/app/javascript/dashboard/api/webhooks.js
@@ -0,0 +1,9 @@
+import ApiClient from './ApiClient';
+
+class WebHooks extends ApiClient {
+ constructor() {
+ super('webhooks', { accountScoped: true });
+ }
+}
+
+export default new WebHooks();
diff --git a/app/javascript/dashboard/assets/audio/ding.mp3 b/app/javascript/dashboard/assets/audio/ding.mp3
deleted file mode 100644
index 1c4921711..000000000
Binary files a/app/javascript/dashboard/assets/audio/ding.mp3 and /dev/null differ
diff --git a/app/javascript/dashboard/assets/images/channels/twilio.png b/app/javascript/dashboard/assets/images/channels/twilio.png
new file mode 100644
index 000000000..627a8e9d4
Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/twilio.png differ
diff --git a/app/javascript/dashboard/assets/images/channels/whatsapp.png b/app/javascript/dashboard/assets/images/channels/whatsapp.png
new file mode 100644
index 000000000..547ff675e
Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/whatsapp.png differ
diff --git a/app/javascript/dashboard/assets/images/integrations/cable.svg b/app/javascript/dashboard/assets/images/integrations/cable.svg
new file mode 100644
index 000000000..2a9f7008d
--- /dev/null
+++ b/app/javascript/dashboard/assets/images/integrations/cable.svg
@@ -0,0 +1,64 @@
+
+
+
diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss
index 4a32d17d1..fa84afa0c 100644
--- a/app/javascript/dashboard/assets/scss/_helper-classes.scss
+++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss
@@ -3,8 +3,8 @@
}
.flex-center {
- display: flex;
@include flex-align(center, middle);
+ display: flex;
}
.bottom-space-fix {
@@ -17,42 +17,43 @@
.spinner {
@include color-spinner();
- position: relative;
display: inline-block;
- width: $space-medium;
height: $space-medium;
padding: $zero $space-medium;
+ position: relative;
vertical-align: middle;
+ width: $space-medium;
&.message {
- padding: $space-normal;
- top: 0;
- left: 0;
- margin: 0 auto;
- margin-top: $space-slab;
+ @include elegent-shadow;
background: $color-white;
border-radius: $space-large;
- @include elegent-shadow;
+ left: 0;
+ margin: $space-slab 0 auto;
+ padding: $space-normal;
+ top: 0;
- &:before {
- margin-top: -$space-slab;
+ &::before {
margin-left: -$space-slab;
+ margin-top: -$space-slab;
}
}
&.small {
- width: $space-normal;
height: $space-normal;
+ width: $space-normal;
- &:before {
- width: $space-normal;
+ &::before {
height: $space-normal;
margin-top: -$space-small;
+ width: $space-normal;
}
}
}
-input, textarea {
+input,
+textarea,
+select {
border-radius: 4px !important;
}
diff --git a/app/javascript/dashboard/assets/scss/_layout.scss b/app/javascript/dashboard/assets/scss/_layout.scss
index e636b2873..cdb81c76a 100644
--- a/app/javascript/dashboard/assets/scss/_layout.scss
+++ b/app/javascript/dashboard/assets/scss/_layout.scss
@@ -35,11 +35,11 @@ body {
flex-direction: column;
@include margin($zero);
@include padding($space-normal);
- overflow-y: scroll;
+ overflow-y: auto;
}
.content-box {
- overflow: scroll;
+ overflow: auto;
@include padding($space-normal);
}
diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss
index 953ab11a4..e4efee5ca 100644
--- a/app/javascript/dashboard/assets/scss/_mixins.scss
+++ b/app/javascript/dashboard/assets/scss/_mixins.scss
@@ -129,17 +129,16 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
}
@mixin scroll-on-hover() {
- transition: all .4s $ease-in-out-cubic;
overflow: hidden;
&:hover {
- overflow-y: scroll;
+ overflow-y: auto;
}
}
@mixin horizontal-scroll() {
- overflow-y: scroll;
+ overflow-y: auto;
}
@mixin elegent-shadow() {
diff --git a/app/javascript/dashboard/assets/scss/_typography.scss b/app/javascript/dashboard/assets/scss/_typography.scss
index 40d1c5a29..8c6d2e0f2 100644
--- a/app/javascript/dashboard/assets/scss/_typography.scss
+++ b/app/javascript/dashboard/assets/scss/_typography.scss
@@ -18,10 +18,14 @@
font-size: $font-size-small;
}
+.text-muted {
+ color: $color-gray;
+}
+
a {
font-size: $font-size-small;
}
p {
font-size: $font-size-small;
-}
\ No newline at end of file
+}
diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss
index c85012e9f..05d822b5a 100644
--- a/app/javascript/dashboard/assets/scss/_woot.scss
+++ b/app/javascript/dashboard/assets/scss/_woot.scss
@@ -9,7 +9,6 @@
@import 'widgets/conv-header';
@import 'widgets/conversation-card';
@import 'widgets/conversation-view';
-@import 'widgets/emojiinput';
@import 'widgets/forms';
@import 'widgets/login';
@import 'widgets/modal';
@@ -25,6 +24,7 @@
@import 'views/settings/inbox';
@import 'views/settings/channel';
+@import 'views/settings/integrations';
@import 'views/signup';
@import 'plugins/multiselect';
diff --git a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss
index b6865213e..438945920 100644
--- a/app/javascript/dashboard/assets/scss/views/settings/inbox.scss
+++ b/app/javascript/dashboard/assets/scss/views/settings/inbox.scss
@@ -31,42 +31,39 @@
.wizard-box {
.item {
@include padding($space-normal $space-normal $space-normal $space-medium);
- position: relative;
@include background-light;
- cursor: pointer;
- &:before,
- &:after {
- content: '';
- position: absolute;
- width: 2px;
- height: 100%;
+ cursor: pointer;
+ position: relative;
+
+ &::before,
+ &::after {
background: $color-border;
+ content: '';
+ height: 100%;
+ position: absolute;
top: $space-normal;
+ width: 2px;
}
- &:before {
- top: $zero;
+ &::before {
height: $space-normal;
+ top: $zero;
}
&:first-child {
- &:before {
+ &::before {
height: 0;
}
}
&:last-child {
- &:after {
+ &::after {
height: $zero;
}
}
&.active {
- // left: 1px;
- // @include background-white;
- // @include border-light;
- // border-right: 0;
h3 {
color: $color-woot;
}
@@ -78,7 +75,7 @@
&.over {
- &:after {
+ &::after {
background: $color-woot;
}
@@ -86,18 +83,18 @@
background: $color-woot;
}
- &+.item {
- &:before {
+ & + .item {
+ &::before {
background: $color-woot;
}
}
}
h3 {
- font-size: $font-size-default;
- padding-left: $space-medium;
- line-height: 1;
color: $color-body;
+ font-size: $font-size-default;
+ line-height: 1;
+ padding-left: $space-medium;
.completed {
color: $success-color;
@@ -105,25 +102,25 @@
}
p {
- font-size: $font-size-small;
color: $color-light-gray;
- padding-left: $space-medium;
+ font-size: $font-size-small;
margin: 0;
+ padding-left: $space-medium;
}
.step {
- position: absolute;
- left: $space-normal;
- top: $space-normal;
- font-size: $font-size-small;
- font-weight: $font-weight-medium;
background: $color-border;
border-radius: 20px;
- width: $space-normal;
+ color: $color-white;
+ font-size: $font-size-small;
+ font-weight: $font-weight-medium;
height: $space-normal;
- text-align: center;
+ left: $space-normal;
line-height: $space-normal;
- color: #fff;
+ position: absolute;
+ text-align: center;
+ top: $space-normal;
+ width: $space-normal;
z-index: 999;
i {
@@ -141,10 +138,6 @@
}
.inoboxes-list {
- // @include margin(auto);
- // @include background-white;
- // @include border-light;
- // width: 50%;
.inbox-item {
@include margin($space-normal);
@@ -152,16 +145,18 @@
@include flex-shrink;
@include padding($space-normal $space-normal);
@include border-light-bottom();
- flex-direction: column;
+
background: $color-white;
cursor: pointer;
- width: 20%;
+ flex-direction: column;
float: left;
min-height: 10rem;
+ width: 20%;
&:last-child {
- margin-bottom: $zero;
@include border-nil;
+
+ margin-bottom: $zero;
}
&:hover {
@@ -174,8 +169,8 @@
.switch {
align-self: center;
- margin-right: $space-normal;
margin-bottom: $zero;
+ margin-right: $space-normal;
}
.item--details {
@@ -187,15 +182,15 @@
}
.item--sub {
- margin-bottom: 0;
font-size: $font-size-small;
+ margin-bottom: 0;
}
}
.arrow {
align-self: center;
- font-size: $font-size-small;
color: $medium-gray;
+ font-size: $font-size-small;
opacity: .7;
transform: translateX(0);
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
@@ -204,18 +199,19 @@
}
.settings--content {
- @include margin($space-small $space-medium);
+ @include margin($space-small $space-larger);
.title {
font-weight: $font-weight-medium;
}
.code {
- max-height: $space-mega;
- overflow: scroll;
- white-space: nowrap;
@include padding($space-one);
+
background: $color-background;
+ max-height: $space-mega;
+ overflow: auto;
+ white-space: nowrap;
code {
background: transparent;
@@ -225,8 +221,8 @@
}
.login-init {
- text-align: center;
padding-top: 30%;
+ text-align: center;
p {
@include padding($space-medium);
diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss
new file mode 100644
index 000000000..183fa9a23
--- /dev/null
+++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss
@@ -0,0 +1,37 @@
+.integrations-wrap {
+ .integration {
+ background: $color-white;
+ border: 2px solid $color-border;
+ border-radius: $space-slab;
+ padding: $space-normal;
+
+ .integration--image {
+ display: flex;
+ margin-right: $space-normal;
+ width: 8rem;
+
+ img {
+ max-width: 8rem;
+ padding: $space-small;
+ }
+ }
+
+ .integration--title {
+ font-size: $font-size-large;
+ }
+
+ .integration--description {
+ padding-right: $space-medium;
+ }
+
+ .button-wrap {
+ @include flex;
+ @include flex-align(center, middle);
+ margin-bottom: 0;
+ }
+ }
+}
+
+.help-wrap {
+ padding-left: $space-large;
+}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss
index f4a037886..0122ba96b 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss
@@ -20,12 +20,12 @@
border-radius: $space-smaller;
margin-right: $space-normal;
- &:before {
- line-height: 3.8rem;
+ &::before {
+ color: $medium-gray;
font-size: $font-size-default;
+ line-height: 3.8rem;
padding-left: $space-slab;
padding-right: $space-smaller;
- color: $medium-gray;
}
.multiselect {
@@ -49,33 +49,32 @@
}
.user--profile__meta {
+ align-items: flex-start;
display: flex;
flex-direction: column;
- align-items: flex-start;
justify-content: center;
margin-left: $space-slab;
}
.user--profile__button {
- color: $color-woot;
font-size: $font-size-mini;
margin-top: $space-micro;
- cursor: pointer;
+ padding: 0;
}
}
}
.button.resolve--button {
>.icon {
- padding-right: $space-small;
font-size: $font-size-default;
+ padding-right: $space-small;
}
.spinner {
- padding: 0 $space-one;
margin-right: $space-smaller;
+ padding: 0 $space-one;
- &:before {
+ &::before {
border-top-color: $color-white;
}
}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
index 649b3b31b..c331492d2 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
@@ -43,6 +43,11 @@
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
+
+ .small-icon {
+ font-size: $font-size-mini;
+ vertical-align: top;
+ }
}
.conversation--meta {
diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
index 7c28e3231..283281cbc 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
@@ -31,26 +31,36 @@
}
.image {
- @include flex;
- align-items: flex-end;
- justify-content: center;
- text-align: center;
-
- img {
- @include padding($space-small);
- max-height: 30rem;
- max-width: 20rem;
- }
+ cursor: pointer;
+ position: relative;
.time {
- margin-left: -$space-large;
+ bottom: $space-smaller;
+ color: $color-white;
+ position: absolute;
+ right: $space-small;
white-space: nowrap;
}
+ .modal-container {
+ text-align: center;
+ }
+
.modal-image {
- max-height: 80%;
max-width: 80%;
}
+
+ &::before {
+ $color-black: #000;
+ background-image: linear-gradient(-180deg, transparent 3%, $color-black 70%);
+ bottom: 0;
+ content: '';
+ height: 20%;
+ left: 0;
+ opacity: .8;
+ position: absolute;
+ width: 100%;
+ }
}
.map {
@@ -82,6 +92,21 @@
@include flex;
flex-direction: column;
+ .load-more-conversations {
+ font-size: $font-size-small;
+ padding: $space-normal;
+ width: 100%;
+ }
+
+ .end-of-list-text {
+ padding: $space-normal;
+ }
+
+ .conversations-list {
+ @include flex-weight(1);
+ @include scroll-on-hover;
+ }
+
.chat-list__top {
@include flex;
@include padding($space-normal $zero $space-small $zero);
@@ -108,10 +133,7 @@
}
}
- .conversations-list {
- @include flex-weight(1);
- @include scroll-on-hover;
- }
+
.content-box {
text-align: center;
@@ -169,7 +191,7 @@
// Firefox flexbox fix
height: 100%;
margin-bottom: $space-small;
- overflow-y: scroll;
+ overflow-y: auto;
li {
@include flex;
diff --git a/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss b/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss
index 8fd11ec4d..f4bc48e7c 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_emojiinput.scss
@@ -1,3 +1,6 @@
+@import '../variables';
+@import '../mixins';
+
.emoji-dialog {
@include elegant-card;
background: $color-white;
@@ -15,15 +18,15 @@
}
.emojione {
- @include margin($zero);
- font-size: $font-size-medium;
+ font-size: $font-size-default;
+ margin: $zero;
}
.emoji-row {
- @include padding($space-small);
box-sizing: border-box;
height: 180px;
- overflow-y: scroll;
+ overflow-y: auto;
+ padding: $space-small;
.emoji {
border-radius: 4px;
@@ -52,27 +55,33 @@
}
.emoji-dialog-header {
- @include padding($zero $space-smaller);
- background-color: $light-gray;
+ background-color: $color-body;
border-top-left-radius: $space-small;
border-top-right-radius: $space-small;
+ padding: $zero $space-smaller;
ul {
+ display: flex;
list-style: none;
margin: 0;
padding: $space-smaller 0 0;
- > li {
- @include padding($space-smaller $space-small);
- box-sizing: border-box;
+ >li {
+ align-items: center;
cursor: pointer;
- display: inline-block;
- height: 3.4rem;
- text-align: center;
+ display: flex;
+ height: $space-medium;
+ justify-content: center;
+ padding: $space-smaller $space-small;
}
- > .active {
- background: $white;
+ .emojione {
+ height: $space-two;
+ width: $space-normal;
+ }
+
+ >.active {
+ background: $color-white;
border-top-left-radius: $space-small;
border-top-right-radius: $space-small;
}
@@ -84,13 +93,14 @@
}
.active {
+
img,
svg {
filter: grayscale(0);
}
}
- > * {
+ >* {
display: table-cell;
vertical-align: middle;
}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_login.scss b/app/javascript/dashboard/assets/scss/widgets/_login.scss
index 608823d39..8720c6b57 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_login.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_login.scss
@@ -5,7 +5,7 @@
// Outside login wrapper
.login {
@include full-height;
- overflow-y: scroll;
+ overflow-y: auto;
padding-top: $space-larger * 1.2;
.login__hero {
diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss
index 5a2cc7b3d..641400b1e 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss
@@ -40,7 +40,7 @@
background-color: $color-white;
border-radius: $space-small;
max-height: 100%;
- overflow: scroll;
+ overflow: auto;
position: relative;
width: 60rem;
diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss
index 11350ceaf..d0bab2074 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss
@@ -24,7 +24,7 @@
left: 0;
max-height: 14rem;
- overflow: scroll;
+ overflow: auto;
position: absolute;
width: 24rem;
z-index: 100;
@@ -47,7 +47,7 @@
}
}
- >.icon {
+ .icon {
color: $medium-gray;
cursor: pointer;
font-size: $font-size-medium;
@@ -58,6 +58,16 @@
}
}
+ .file-uploads>label {
+ cursor: pointer;
+ }
+
+ .attachment {
+ cursor: pointer;
+ margin-right: $space-one;
+ padding: 0 $space-small;
+ }
+
>textarea {
@include ghost-input();
@include margin(0);
diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss
index ef5e1dea9..2d1fd7511 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss
@@ -28,9 +28,16 @@
color: $color-gray;
font-size: $font-size-default;
font-weight: $font-weight-medium;
+
+ .wrap,
+ .child-icon {
+ &:hover {
+ color: $color-woot;
+ }
+ }
}
- .active a {
+ .active a .wrap {
color: $color-woot;
}
}
@@ -100,7 +107,7 @@
margin-top: $space-medium;
>span {
- margin-left: auto;
+ margin-left: $space-one;
}
}
}
diff --git a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
index 0e76ffde9..e7d261954 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss
@@ -21,7 +21,17 @@
}
.tabs-title {
- @include margin($zero $space-one);
+ @include margin($zero $space-slab);
+
+ .badge {
+ background: $color-background;
+ border-radius: $space-small;
+ color: $color-gray;
+ font-size: $font-size-micro;
+ font-weight: $font-weight-black;
+ margin-left: $space-smaller;
+ padding: $space-smaller;
+ }
&:first-child {
margin-left: 0;
@@ -40,10 +50,13 @@
a {
@include position(relative, 1px null null null);
- transition: all .15s $ease-in-out-cubic;
+ align-items: center;
border-bottom: 2px solid transparent;
color: $medium-gray;
+ display: flex;
+ flex-direction: row;
font-size: $font-size-small;
+ transition: all .15s $ease-in-out-cubic;
}
&.is-active {
@@ -51,5 +64,10 @@
border-bottom-color: $color-woot;
color: $color-woot;
}
+
+ .badge {
+ background: $color-extra-light-blue;
+ color: $color-woot;
+ }
}
}
diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue
index 6f69857f7..c0ddb1987 100644
--- a/app/javascript/dashboard/components/ChatList.vue
+++ b/app/javascript/dashboard/components/ChatList.vue
@@ -3,40 +3,52 @@
- {{ inbox.name || pageTitle }}
+ {{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}
-
+
-
+
{{ $t('CHAT_LIST.LIST.404') }}
-
-
-
-
-
+
-
+
+
+
+
+
+
+ {{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
+
+
+
+ {{ $t('CHAT_LIST.EOF') }}
+
+
@@ -59,11 +71,11 @@ export default {
ChatFilter,
},
mixins: [timeMixin, conversationMixin],
- props: ['conversationInbox', 'pageTitle'],
+ props: ['conversationInbox'],
data() {
return {
- activeAssigneeTab: 0,
- activeStatus: 0,
+ activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
+ activeStatus: wootConstants.STATUS_TYPE.OPEN,
};
},
computed: {
@@ -78,66 +90,69 @@ export default {
convStats: 'getConvTabStats',
}),
assigneeTabItems() {
- return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map((item, index) => ({
- id: index,
+ return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({
+ key: item.KEY,
name: item.NAME,
- count: this.convStats[item.KEY] || 0,
+ count: this.convStats[item.COUNT_KEY] || 0,
}));
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
},
- getToggleStatus() {
- if (this.toggleType) {
- return 'Open';
- }
- return 'Resolved';
+ currentPage() {
+ return this.$store.getters['conversationPage/getCurrentPage'](
+ this.activeAssigneeTab
+ );
+ },
+ hasCurrentPageEndReached() {
+ return this.$store.getters['conversationPage/getHasEndReached'](
+ this.activeAssigneeTab
+ );
+ },
+ },
+ watch: {
+ conversationInbox() {
+ this.resetAndFetchData();
},
},
mounted() {
- this.$watch('$store.state.route', () => {
- if (this.$store.state.route.name !== 'inbox_conversation') {
- this.$store.dispatch('emptyAllConversations');
- this.fetchData();
- }
- });
-
- this.$store.dispatch('emptyAllConversations');
- this.fetchData();
+ this.$store.dispatch('setChatFilter', this.activeStatus);
+ this.resetAndFetchData();
this.$store.dispatch('agents/get');
},
methods: {
- fetchData() {
- if (this.chatLists.length === 0) {
- this.fetchConversations();
- }
+ resetAndFetchData() {
+ this.$store.dispatch('conversationPage/reset');
+ this.$store.dispatch('emptyAllConversations');
+ this.fetchConversations();
},
fetchConversations() {
this.$store.dispatch('fetchAllConversations', {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
- status: this.activeStatus ? 'resolved' : 'open',
+ status: this.activeStatus,
+ page: this.currentPage + 1,
});
},
- getDataForTab(index) {
- if (this.activeAssigneeTab !== index) {
- this.activeAssigneeTab = index;
- this.fetchConversations();
+ updateAssigneeTab(selectedTab) {
+ if (this.activeAssigneeTab !== selectedTab) {
+ this.activeAssigneeTab = selectedTab;
+ if (!this.currentPage) {
+ this.fetchConversations();
+ }
}
},
- getDataForStatusTab(index) {
+ updateStatusType(index) {
if (this.activeStatus !== index) {
this.activeStatus = index;
- this.fetchConversations();
+ this.resetAndFetchData();
}
},
getChatsForTab() {
let copyList = [];
- if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.MINE) {
+ if (this.activeAssigneeTab === 'me') {
copyList = this.mineChatsList.slice();
- } else if (
- this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.UNASSIGNED
- ) {
+ } else if (this.activeAssigneeTab === 'unassigned') {
copyList = this.unAssignedChatsList.slice();
} else {
copyList = this.allChatList.slice();
diff --git a/app/javascript/dashboard/components/Code.vue b/app/javascript/dashboard/components/Code.vue
index 17134ebdc..95b09e82c 100644
--- a/app/javascript/dashboard/components/Code.vue
+++ b/app/javascript/dashboard/components/Code.vue
@@ -26,7 +26,8 @@ export default {
},
},
methods: {
- onCopy() {
+ onCopy(e) {
+ e.preventDefault();
copy(this.script);
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue
index b3179a641..b7d9489fb 100644
--- a/app/javascript/dashboard/components/Modal.vue
+++ b/app/javascript/dashboard/components/Modal.vue
@@ -1,6 +1,11 @@
-
+
@@ -12,9 +17,19 @@
diff --git a/app/javascript/dashboard/components/SettingsFormHeader.vue b/app/javascript/dashboard/components/SettingsFormHeader.vue
deleted file mode 100644
index 795751612..000000000
--- a/app/javascript/dashboard/components/SettingsFormHeader.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue
new file mode 100644
index 000000000..ebf79bf3e
--- /dev/null
+++ b/app/javascript/dashboard/components/SettingsSection.vue
@@ -0,0 +1,46 @@
+
+
+
+
+ {{ title }}
+
+
+ {{ subTitle }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/buttons/ResolveButton.vue b/app/javascript/dashboard/components/buttons/ResolveButton.vue
index d8ca582cb..9b1181426 100644
--- a/app/javascript/dashboard/components/buttons/ResolveButton.vue
+++ b/app/javascript/dashboard/components/buttons/ResolveButton.vue
@@ -16,8 +16,12 @@
/* global bus */
import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner';
+import wootConstants from '../../constants';
export default {
+ components: {
+ Spinner,
+ },
props: ['conversationId'],
data() {
return {
@@ -29,19 +33,23 @@ export default {
currentChat: 'getSelectedChat',
}),
currentStatus() {
- const ButtonName = this.currentChat.status === 0 ? 'Resolve' : 'Reopen';
+ const ButtonName =
+ this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
+ ? this.$t('CONVERSATION.HEADER.RESOLVE_ACTION')
+ : this.$t('CONVERSATION.HEADER.REOPEN_ACTION');
return ButtonName;
},
buttonClass() {
- return this.currentChat.status === 0 ? 'success' : 'warning';
+ return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
+ ? 'success'
+ : 'warning';
},
buttonIconClass() {
- return this.currentChat.status === 0 ? 'ion-checkmark' : 'ion-refresh';
+ return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
+ ? 'ion-checkmark'
+ : 'ion-refresh';
},
},
- components: {
- Spinner,
- },
methods: {
toggleStatus() {
this.isLoading = true;
diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue
index 52838fa3f..dfb9081d2 100644
--- a/app/javascript/dashboard/components/layout/Sidebar.vue
+++ b/app/javascript/dashboard/components/layout/Sidebar.vue
@@ -43,13 +43,13 @@
>
@@ -139,23 +139,23 @@ export default {
inboxSection() {
return {
icon: 'ion-folder',
- label: 'Inboxes',
+ label: 'INBOXES',
hasSubMenu: true,
newLink: true,
key: 'inbox',
cssClass: 'menu-title align-justify',
- toState: frontendURL('settings/inboxes'),
+ toState: frontendURL(`accounts/${this.accountId}/settings/inboxes`),
toStateName: 'settings_inbox_list',
children: this.inboxes.map(inbox => ({
id: inbox.id,
label: inbox.name,
- toState: frontendURL(`inbox/${inbox.id}`),
+ toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
type: inbox.channel_type,
})),
};
},
dashboardPath() {
- return frontendURL('dashboard');
+ return frontendURL(`accounts/${this.accountId}/dashboard`);
},
shouldShowStatusBox() {
return (
@@ -176,6 +176,9 @@ export default {
trialMessage() {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
+ accountId() {
+ return this.currentUser.account_id;
+ },
},
mounted() {
this.$store.dispatch('inboxes/get');
diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue
index 2340d1f7f..8e8297e01 100644
--- a/app/javascript/dashboard/components/layout/SidebarItem.vue
+++ b/app/javascript/dashboard/components/layout/SidebarItem.vue
@@ -6,16 +6,19 @@
:class="computedClass"
>
@@ -28,12 +31,14 @@
:to="child.toState"
>
-
- {{ child.label }}
+
+
+ {{ child.label }}
+
@@ -51,6 +56,7 @@ const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
+ TWILIO: 'Channel::TwilioSms',
};
const getInboxClassByType = type => {
switch (type) {
@@ -63,6 +69,9 @@ const getInboxClassByType = type => {
case INBOX_TYPES.TWITTER:
return 'ion-social-twitter';
+ case INBOX_TYPES.TWILIO:
+ return 'ion-android-textsms';
+
default:
return '';
}
@@ -115,3 +124,9 @@ export default {
},
};
+
diff --git a/app/javascript/dashboard/components/ui/Tabs/TabsItem.js b/app/javascript/dashboard/components/ui/Tabs/TabsItem.js
index 929d7897d..3c57abdc3 100644
--- a/app/javascript/dashboard/components/ui/Tabs/TabsItem.js
+++ b/app/javascript/dashboard/components/ui/Tabs/TabsItem.js
@@ -80,7 +80,8 @@ export default {
}
}}
>
- {`${this.name} (${this.getItemCount})`}
+ {`${this.name}`}
+
{this.getItemCount}
);
diff --git a/app/javascript/dashboard/components/ui/Wizard.vue b/app/javascript/dashboard/components/ui/Wizard.vue
index 8b0d46ed4..f34b8f182 100644
--- a/app/javascript/dashboard/components/ui/Wizard.vue
+++ b/app/javascript/dashboard/components/ui/Wizard.vue
@@ -29,12 +29,6 @@
/* eslint no-console: 0 */
export default {
props: {
- items: {
- type: Array,
- default() {
- return [];
- },
- },
isFullwidth: Boolean,
},
@@ -45,6 +39,9 @@ export default {
activeIndex() {
return this.items.findIndex(i => i.route === this.$route.name);
},
+ items() {
+ return this.$t('INBOX_MGMT.CREATE_FLOW');
+ },
},
methods: {
isActive(item) {
diff --git a/app/javascript/dashboard/components/widgets/Avatar.vue b/app/javascript/dashboard/components/widgets/Avatar.vue
index b23b5a737..5489278d5 100644
--- a/app/javascript/dashboard/components/widgets/Avatar.vue
+++ b/app/javascript/dashboard/components/widgets/Avatar.vue
@@ -81,5 +81,6 @@ export default {
align-items: center;
justify-content: center;
text-align: center;
+ background-image: linear-gradient(to top, #4481eb 0%, #04befe 100%);
}
diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue
index da22f71bd..948bff103 100644
--- a/app/javascript/dashboard/components/widgets/ChannelItem.vue
+++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue
@@ -24,6 +24,10 @@
v-if="channel === 'website'"
src="~dashboard/assets/images/channels/website.png"
/>
+
{{ channel }}
@@ -39,7 +43,7 @@ export default {
},
methods: {
isActive(channel) {
- return ['facebook', 'website', 'twitter'].includes(channel);
+ return ['facebook', 'website', 'twitter', 'twilio'].includes(channel);
},
onItemClick() {
if (this.isActive(this.channel)) {
diff --git a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
index 3615543c6..0ed5e542b 100644
--- a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
+++ b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
@@ -1,15 +1,15 @@
-
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 7e7bacfd2..7d21d2394 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -23,6 +23,18 @@
@click="onClick()"
@blur="onBlur()"
/>
+
+
+
+
@@ -49,16 +65,12 @@
class="button send-button"
:disabled="disableButton()"
:class="{
- disabled: message.length === 0 || message.length > 640,
+ disabled: message.length === 0 || message.length > maxLength,
warning: isPrivate,
}"
@click="sendMessage"
>
- {{
- isPrivate
- ? $t('CONVERSATION.REPLYBOX.CREATE')
- : $t('CONVERSATION.REPLYBOX.SEND')
- }}
+ {{ replyButtonLabel }}
+
+
+
+
+ {{ readableTime }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue b/app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue
index 5fd762bcf..693680281 100644
--- a/app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue
+++ b/app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue
@@ -3,37 +3,39 @@
-
{{selectedKey}}
+
+ {{ selectedKey }}
+
-
+
diff --git a/app/javascript/dashboard/constants.js b/app/javascript/dashboard/constants.js
index 1f1b5cda4..35559648c 100644
--- a/app/javascript/dashboard/constants.js
+++ b/app/javascript/dashboard/constants.js
@@ -4,9 +4,13 @@ export default {
return `${this.APP_BASE_URL}/`;
},
GRAVATAR_URL: 'https://www.gravatar.com/avatar/',
- ASSIGNEE_TYPE_SLUG: {
- MINE: 0,
- UNASSIGNED: 1,
- OPEN: 0,
+ ASSIGNEE_TYPE: {
+ ME: 'me',
+ UNASSIGNED: 'unassigned',
+ ALL: 'all',
+ },
+ STATUS_TYPE: {
+ OPEN: 'open',
+ RESOLVED: 'resolved',
},
};
diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js
index 5448a9d6a..5e6cae568 100644
--- a/app/javascript/dashboard/helper/URLHelper.js
+++ b/app/javascript/dashboard/helper/URLHelper.js
@@ -4,3 +4,19 @@ export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`;
};
+
+export const conversationUrl = (accountId, activeInbox, id) => {
+ const path = activeInbox
+ ? `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`
+ : `accounts/${accountId}/conversations/${id}`;
+ return path;
+};
+
+export const accountIdFromPathname = pathname => {
+ const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
+ const urlParam = pathname.split('/')[3];
+ // eslint-disable-next-line no-restricted-globals
+ const isScoped = isInsideAccountScopedURLs && !isNaN(urlParam);
+ const accountId = isScoped ? Number(urlParam) : '';
+ return accountId;
+};
diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js
index 27ff80dbc..a1f70702e 100644
--- a/app/javascript/dashboard/helper/actionCable.js
+++ b/app/javascript/dashboard/helper/actionCable.js
@@ -6,14 +6,20 @@ class ActionCableConnector extends BaseActionCableConnector {
super(app, pubsubToken);
this.events = {
'message.created': this.onMessageCreated,
+ 'message.updated': this.onMessageUpdated,
'conversation.created': this.onConversationCreated,
- 'status_change:conversation': this.onStatusChange,
+ 'conversation.opened': this.onStatusChange,
+ 'conversation.resolved': this.onStatusChange,
'user:logout': this.onLogout,
'page:reload': this.onReload,
'assignee.changed': this.onAssigneeChanged,
};
}
+ onMessageUpdated = data => {
+ this.app.$store.dispatch('updateMessage', data);
+ };
+
onAssigneeChanged = payload => {
const { meta = {}, id } = payload;
const { assignee } = meta || {};
@@ -35,7 +41,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onReload = () => window.location.reload();
onStatusChange = data => {
- this.app.$store.dispatch('addConversation', data);
+ this.app.$store.dispatch('updateConversation', data);
};
}
diff --git a/app/javascript/dashboard/helper/scriptGenerator.js b/app/javascript/dashboard/helper/scriptGenerator.js
index bfb900718..5a278d30a 100644
--- a/app/javascript/dashboard/helper/scriptGenerator.js
+++ b/app/javascript/dashboard/helper/scriptGenerator.js
@@ -1,20 +1,3 @@
-export const createWebsiteWidgetScript = websiteToken => `
-
-`;
-
export const createMessengerScript = pageId => `
@@ -165,8 +184,16 @@ export default {
overflow-y: auto;
background: white;
overflow: auto;
+ position: relative;
}
+.close-button {
+ position: absolute;
+ right: $space-slab;
+ top: $space-slab;
+ font-size: $font-size-default;
+ color: $color-heading;
+}
.contact--profile {
padding: $space-medium $space-normal 0 $space-medium;
align-items: center;
@@ -191,7 +218,7 @@ export default {
text-transform: capitalize;
font-weight: $font-weight-bold;
- font-size: $font-size-medium;
+ font-size: $font-size-default;
}
.contact--email {
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue
index a0f4cb42d..c51053c19 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue
@@ -1,10 +1,6 @@
@@ -36,7 +33,6 @@ export default {
data() {
return {
- pageTitle: this.$state,
panelToggleState: false,
};
},
@@ -59,7 +55,16 @@ export default {
props: ['inboxId', 'conversationId'],
mounted() {
- this.$watch('$store.state.route', () => {
+ this.initialize();
+ this.$watch('$store.state.route', () => this.initialize());
+ this.$watch('chatList.length', () => {
+ this.fetchConversation();
+ this.setActiveChat();
+ });
+ },
+
+ methods: {
+ initialize() {
switch (this.$store.state.route.name) {
case 'inbox_conversation':
this.setActiveChat();
@@ -69,20 +74,36 @@ export default {
this.$store.dispatch('setActiveInbox', this.inboxId);
}
break;
+ case 'conversation_through_inbox':
+ if (this.inboxId) {
+ this.$store.dispatch('setActiveInbox', this.inboxId);
+ }
+ this.setActiveChat();
+ break;
default:
this.$store.dispatch('setActiveInbox', null);
+ this.$store.dispatch('clearSelectedState');
break;
}
- });
- this.$watch('chatList.length', () => {
- this.setActiveChat();
- });
- },
+ },
- methods: {
- setActiveChat() {
+ fetchConversation() {
+ if (!this.conversationId) {
+ return;
+ }
+ const chat = this.findConversation();
+ if (!chat) {
+ this.$store.dispatch('getConversation', this.conversationId);
+ }
+ },
+ findConversation() {
const conversationId = parseInt(this.conversationId, 10);
const [chat] = this.chatList.filter(c => c.id === conversationId);
+ return chat;
+ },
+
+ setActiveChat() {
+ const chat = this.findConversation();
if (!chat) return;
this.$store.dispatch('setActiveChat', chat).then(() => {
bus.$emit('scrollToMessage');
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js
index 8b47e1304..0a51c24c2 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js
@@ -5,7 +5,7 @@ import { frontendURL } from '../../../helper/URLHelper';
export default {
routes: [
{
- path: frontendURL('dashboard'),
+ path: frontendURL('accounts/:accountId/dashboard'),
name: 'home',
roles: ['administrator', 'agent'],
component: ConversationView,
@@ -14,7 +14,7 @@ export default {
},
},
{
- path: frontendURL('inbox/:inbox_id'),
+ path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
component: ConversationView,
@@ -23,12 +23,26 @@ export default {
},
},
{
- path: frontendURL('conversations/:conversation_id'),
+ path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
- return { conversationId: route.params.conversation_id };
+ return { inboxId: 0, conversationId: route.params.conversation_id };
+ },
+ },
+ {
+ path: frontendURL(
+ 'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
+ ),
+ name: 'conversation_through_inbox',
+ roles: ['administrator', 'agent'],
+ component: ConversationView,
+ props: route => {
+ return {
+ conversationId: route.params.conversation_id,
+ inboxId: route.params.inbox_id,
+ };
},
},
],
diff --git a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
index 9fd87d851..42db278c7 100644
--- a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
@@ -6,7 +6,7 @@ import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
- path: frontendURL(''),
+ path: frontendURL('accounts/:account_id'),
component: AppContainer,
children: [...conversation.routes, ...settings.routes],
},
diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue
index fc1028007..76858695c 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue
@@ -2,12 +2,12 @@