Merge branch 'release/1.3.0'
This commit is contained in:
commit
e04b37dfcf
422 changed files with 8393 additions and 2214 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
27
.env.example
27
.env.example
|
@ -22,17 +22,6 @@ 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
|
||||
MAILER_SENDER_EMAIL=accounts@chatwoot.com
|
||||
SMTP_PORT=1025
|
||||
|
@ -59,13 +48,25 @@ AWS_REGION=
|
|||
SENTRY_DSN=
|
||||
|
||||
#Log settings
|
||||
LOG_LEVEL=
|
||||
LOG_SIZE=
|
||||
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=
|
||||
|
||||
|
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
12.16.1
|
62
.rubocop.yml
62
.rubocop.yml
|
@ -41,14 +41,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/**/*'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.6.5
|
||||
2.7.0
|
||||
|
|
|
@ -82,7 +82,7 @@ linters:
|
|||
enabled: true
|
||||
|
||||
ImportantRule:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
||||
ImportPath:
|
||||
enabled: true
|
||||
|
|
7
Gemfile
7
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'
|
||||
|
@ -25,11 +25,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 +62,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
|
||||
|
|
155
Gemfile.lock
155
Gemfile.lock
|
@ -1,6 +1,6 @@
|
|||
GIT
|
||||
remote: https://github.com/chatwoot/twitty
|
||||
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
|
||||
revision: af4f3e45dca55e42c64f7741a1fedfaa94d36419
|
||||
specs:
|
||||
twitty (0.1.0)
|
||||
oauth
|
||||
|
@ -16,7 +16,7 @@ GIT
|
|||
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.2)
|
||||
actionpack (= 6.0.2.2)
|
||||
|
@ -77,46 +77,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-partitions (1.294.0)
|
||||
aws-sdk-core (3.92.0)
|
||||
aws-eventstream (~> 1.0, >= 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.0)
|
||||
browser (4.0.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -127,7 +125,7 @@ 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)
|
||||
|
@ -151,7 +149,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)
|
||||
|
@ -165,23 +163,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)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.0)
|
||||
foreman (0.87.1)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.36.4)
|
||||
google-api-client (0.37.2)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
|
@ -192,8 +190,8 @@ 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)
|
||||
addressable (~> 2.5)
|
||||
|
@ -202,20 +200,22 @@ GEM
|
|||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.10.0)
|
||||
faraday (~> 0.12)
|
||||
googleauth (0.11.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)
|
||||
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)
|
||||
|
@ -224,11 +224,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)
|
||||
|
@ -246,8 +246,8 @@ 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)
|
||||
|
@ -261,7 +261,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)
|
||||
|
@ -271,7 +271,7 @@ GEM
|
|||
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)
|
||||
|
@ -282,14 +282,14 @@ GEM
|
|||
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.0)
|
||||
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.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.3)
|
||||
|
@ -345,7 +345,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)
|
||||
|
@ -360,15 +360,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)
|
||||
|
@ -377,19 +378,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.1)
|
||||
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)
|
||||
|
@ -397,25 +400,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)
|
||||
|
@ -443,7 +447,11 @@ GEM
|
|||
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)
|
||||
|
@ -452,10 +460,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.1)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
|
@ -470,10 +478,11 @@ 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)
|
||||
|
@ -489,7 +498,7 @@ DEPENDENCIES
|
|||
annotate
|
||||
attr_extras
|
||||
aws-sdk-s3
|
||||
azure-storage
|
||||
azure-storage-blob
|
||||
bootsnap
|
||||
brakeman
|
||||
browser
|
||||
|
@ -506,6 +515,7 @@ DEPENDENCIES
|
|||
flag_shih_tzu
|
||||
foreman
|
||||
google-cloud-storage
|
||||
groupdate
|
||||
haikunator
|
||||
hashie
|
||||
jbuilder
|
||||
|
@ -545,6 +555,7 @@ DEPENDENCIES
|
|||
spring-watcher-listen
|
||||
telegram-bot-ruby
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twitty!
|
||||
tzinfo-data
|
||||
uglifier
|
||||
|
@ -554,7 +565,7 @@ DEPENDENCIES
|
|||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.5p114
|
||||
ruby 2.7.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.2
|
||||
2.1.2
|
||||
|
|
47
app/actions/contact_identify_action.rb
Normal file
47
app/actions/contact_identify_action.rb
Normal file
|
@ -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
|
|
@ -9,6 +9,7 @@ class ContactMergeAction
|
|||
merge_contact_inboxes
|
||||
remove_mergee_contact
|
||||
end
|
||||
@base_contact
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -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(' ')
|
||||
|
|
37
app/builders/contact_builder.rb
Normal file
37
app/builders/contact_builder.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
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[:identifier]
|
||||
)
|
||||
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
|
|
@ -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,9 +34,7 @@ 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
|
||||
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
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]
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@fb_id = params[:fb_id]
|
||||
@attachment = params[:attachment]
|
||||
end
|
||||
|
||||
def perform
|
||||
@message = @conversation.messages.create!(message_params)
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachment
|
||||
@message.attachment = Attachment.new(
|
||||
account_id: message.account_id,
|
||||
file_type: file_type(@attachment[:file]&.content_type)
|
||||
)
|
||||
@message.attachment.file.attach(@attachment[:file])
|
||||
end
|
||||
@message.save
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -22,7 +33,7 @@ class Messages::Outgoing::NormalBuilder
|
|||
message_type: :outgoing,
|
||||
content: @content,
|
||||
private: @private,
|
||||
user_id: @user.id,
|
||||
user_id: @user&.id,
|
||||
source_id: @fb_id
|
||||
}
|
||||
end
|
||||
|
|
110
app/builders/v2/report_builder.rb
Normal file
110
app/builders/v2/report_builder.rb
Normal file
|
@ -0,0 +1,110 @@
|
|||
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
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.incoming
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.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
|
|
@ -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?
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class Api::V1::AccountsController < Api::BaseController
|
||||
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
|
||||
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,
|
||||
|
@ -18,18 +20,32 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
).perform
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render json: {
|
||||
data: @user.token_validation_response
|
||||
}
|
||||
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))
|
||||
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)
|
||||
params.permit(:account_name, :email, :name, :locale)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
|
@ -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]
|
||||
|
69
app/controllers/api/v1/accounts/agents_controller.rb
Normal file
69
app/controllers/api/v1/accounts/agents_controller.rb
Normal file
|
@ -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
|
|
@ -1,6 +1,4 @@
|
|||
require 'rest-client'
|
||||
require 'telegram/bot'
|
||||
class Api::V1::CallbacksController < Api::BaseController
|
||||
class Api::V1::Accounts::CallbacksController < Api::BaseController
|
||||
before_action :inbox, only: [:reauthorize_page]
|
||||
|
||||
def register_facebook_page
|
||||
|
@ -18,7 +16,7 @@ class Api::V1::CallbacksController < Api::BaseController
|
|||
render json: inbox
|
||||
end
|
||||
|
||||
def get_facebook_pages
|
||||
def facebook_pages
|
||||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
|
@ -61,13 +59,15 @@ class Api::V1::CallbacksController < Api::BaseController
|
|||
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|
|
||||
current_account.facebook_pages.exists?(page_id: page_detail['id']) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
|
||||
page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
|
||||
result << page_detail
|
||||
end
|
||||
end
|
||||
|
@ -90,11 +90,12 @@ class Api::V1::CallbacksController < Api::BaseController
|
|||
response = uri.open(redirect: false)
|
||||
rescue OpenURI::HTTPRedirect => e
|
||||
uri = e.uri # assigned from the "Location" response header
|
||||
retry if (tries -= 1) > 0
|
||||
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
|
|
@ -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
|
|
@ -0,0 +1,50 @@
|
|||
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController
|
||||
before_action :authorize_request
|
||||
|
||||
def create
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
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 build_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
twilio_sms = current_account.twilio_sms.create(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
phone_number: permitted_params[:phone_number]
|
||||
)
|
||||
@inbox = current_account.inboxes.create(
|
||||
name: permitted_params[:name],
|
||||
channel: twilio_sms
|
||||
)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:twilio_channel).permit(
|
||||
:account_id, :phone_number, :account_sid, :auth_token, :name
|
||||
)
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::Conversations::MessagesController < Api::BaseController
|
||||
class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController
|
||||
before_action :set_conversation, only: [:index, :create]
|
||||
|
||||
def index
|
|
@ -1,5 +1,5 @@
|
|||
class Api::V1::ConversationsController < Api::BaseController
|
||||
before_action :set_conversation, except: [:index]
|
||||
class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
before_action :conversation, except: [:index]
|
||||
|
||||
def index
|
||||
result = conversation_finder.perform
|
||||
|
@ -25,7 +25,7 @@ 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::InboxesController < Api::BaseController
|
||||
class Api::V1::Accounts::InboxesController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_inbox, only: [:destroy, :update]
|
||||
|
||||
|
@ -6,15 +6,15 @@ class Api::V1::InboxesController < Api::BaseController
|
|||
@inboxes = policy_scope(current_account.inboxes)
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@inbox.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_inbox
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::User::NotificationSettingsController < Api::BaseController
|
||||
class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
|
||||
before_action :set_user, :load_notification_setting
|
||||
|
||||
def show; end
|
|
@ -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,
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::Account::WebhooksController < Api::BaseController
|
||||
class Api::V1::Accounts::WebhooksController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_webhook, only: [:update, :destroy]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
class Api::V1::Widget::InboxesController < Api::BaseController
|
||||
class Api::V1::Accounts::Widget::InboxesController < Api::BaseController
|
||||
before_action :authorize_request
|
||||
before_action :set_web_widget_channel, only: [:update]
|
||||
before_action :set_inbox, only: [:update]
|
|
@ -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
|
|
@ -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
|
||||
|
|
18
app/controllers/api/v1/widget/contacts_controller.rb
Normal file
18
app/controllers/api/v1/widget/contacts_controller.rb
Normal file
|
@ -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
|
24
app/controllers/api/v1/widget/labels_controller.rb
Normal file
24
app/controllers/api/v1/widget/labels_controller.rb
Normal file
|
@ -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
|
|
@ -10,20 +10,29 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||
|
||||
def create
|
||||
@message = conversation.messages.new(message_params)
|
||||
build_attachment
|
||||
@message.save!
|
||||
render json: @message
|
||||
end
|
||||
|
||||
def update
|
||||
@message.update!(input_submitted_email: contact_email)
|
||||
update_contact(contact_email)
|
||||
head :no_content
|
||||
rescue StandardError => e
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_attachment
|
||||
return if params[:message][:attachment].blank?
|
||||
|
||||
@message.attachment = Attachment.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(params[:message][:attachment][:file]&.content_type)
|
||||
)
|
||||
@message.attachment.file.attach(params[:message][:attachment][:file])
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
||||
end
|
||||
|
@ -86,7 +95,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,
|
||||
|
|
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
39
app/controllers/api/v2/accounts/reports_controller.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
25
app/controllers/concerns/access_token_auth_helper.rb
Normal file
25
app/controllers/concerns/access_token_auth_helper.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => ['toggle_status'],
|
||||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
}.freeze
|
||||
|
||||
def authenticate_access_token!
|
||||
access_token = AccessToken.find_by(token: request.headers[:api_access_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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
29
app/controllers/twilio/callback_controller.rb
Normal file
29
app/controllers/twilio/callback_controller.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
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
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
class Twitter::CallbacksController < Twitter::BaseController
|
||||
def show
|
||||
return redirect_to app_new_twitter_inbox_url if permitted_params[:denied]
|
||||
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
|
||||
|
||||
@response = twitter_client.access_token(
|
||||
oauth_token: permitted_params[:oauth_token],
|
||||
|
@ -10,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
|
||||
|
||||
|
@ -30,6 +30,10 @@ 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(
|
||||
|
|
|
@ -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 = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners = [AgentBotListener.instance, EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners << EventListener.instance
|
||||
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||
listeners
|
||||
end
|
||||
|
|
14
app/helpers/file_type_helper.rb
Normal file
14
app/helpers/file_type_helper.rb
Normal file
|
@ -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
|
|
@ -8,7 +8,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
@ -17,8 +20,28 @@ export default {
|
|||
WootSnackbarBox,
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
}),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.initializeAccount();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initializeAccount() {
|
||||
const { pathname } = window.location;
|
||||
const accountId = accountIdFromPathname(pathname);
|
||||
|
||||
if (accountId) {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const { locale } = this.getAccount(accountId);
|
||||
Vue.config.lang = locale;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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() {
|
||||
|
|
9
app/javascript/dashboard/api/account.js
Normal file
9
app/javascript/dashboard/api/account.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class AccountAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountAPI();
|
|
@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents');
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class CannedResponse extends ApiClient {
|
||||
constructor() {
|
||||
super('canned_responses');
|
||||
super('canned_responses', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ searchKey }) {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
9
app/javascript/dashboard/api/channel/twilioChannel.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from '../ApiClient';
|
||||
|
||||
class TwilioChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('channels/twilio_channel', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new TwilioChannel();
|
|
@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
|
|||
|
||||
class WebChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('widget/inboxes');
|
||||
super('widget/inboxes', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class ContactAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('contacts');
|
||||
super('contacts', { accountScoped: true });
|
||||
}
|
||||
|
||||
getConversations(contactId) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class ConversationApi extends ApiClient {
|
||||
constructor() {
|
||||
super('conversations');
|
||||
super('conversations', { accountScoped: true });
|
||||
}
|
||||
|
||||
getLabels(conversationID) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from '../ApiClient';
|
|||
|
||||
class ConversationApi extends ApiClient {
|
||||
constructor() {
|
||||
super('conversations');
|
||||
super('conversations', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ inboxId, status, assigneeType, page }) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import ApiClient from '../ApiClient';
|
|||
|
||||
class MessageApi extends ApiClient {
|
||||
constructor() {
|
||||
super('conversations');
|
||||
super('conversations', { accountScoped: true });
|
||||
}
|
||||
|
||||
create({ conversationId, message, private: isPrivate }) {
|
||||
|
@ -19,6 +19,16 @@ class MessageApi extends ApiClient {
|
|||
params: { before },
|
||||
});
|
||||
}
|
||||
|
||||
sendAttachment([conversationId, { file }]) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment[file]', file);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
data: formData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new MessageApi();
|
||||
|
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class InboxMembers extends ApiClient {
|
||||
constructor() {
|
||||
super('inbox_members');
|
||||
super('inbox_members', { accountScoped: true });
|
||||
}
|
||||
|
||||
create({ inboxId, agentList }) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class Inboxes extends ApiClient {
|
||||
constructor() {
|
||||
super('inboxes');
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
15
app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
Normal file
15
app/javascript/dashboard/api/specs/channel/fbChannel.spec.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -3,7 +3,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class UserNotificationSettings extends ApiClient {
|
||||
constructor() {
|
||||
super('user/notification_settings');
|
||||
super('notification_settings', { accountScoped: true });
|
||||
}
|
||||
|
||||
update(params) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class WebHooks extends ApiClient {
|
||||
constructor() {
|
||||
super('account/webhooks');
|
||||
super('webhooks', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/twilio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,11 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 27rem;
|
||||
|
||||
.small-icon {
|
||||
font-size: $font-size-mini;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--meta {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -83,18 +93,12 @@
|
|||
flex-direction: column;
|
||||
|
||||
.load-more-conversations {
|
||||
color: $color-woot;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-small;
|
||||
padding: $space-normal;
|
||||
|
||||
&:hover {
|
||||
background: $color-background;
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.end-of-list-text {
|
||||
font-style: italic;
|
||||
padding: $space-normal;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div
|
||||
v-if="!hasCurrentPageEndReached && !chatListLoading"
|
||||
class="text-center load-more-conversations"
|
||||
class="clear button load-more-conversations"
|
||||
@click="fetchConversations"
|
||||
>
|
||||
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
|
||||
|
|
|
@ -26,7 +26,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onCopy() {
|
||||
onCopy(e) {
|
||||
e.preventDefault();
|
||||
copy(this.script);
|
||||
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
|
|
|
@ -43,13 +43,13 @@
|
|||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li>
|
||||
<router-link to="/app/profile/settings">
|
||||
{{ $t('SIDEBAR.PROFILE_SETTINGS') }}
|
||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" @click.prevent="logout()">
|
||||
{{ $t('SIDEBAR.LOGOUT') }}
|
||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -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');
|
||||
|
|
|
@ -6,16 +6,19 @@
|
|||
:class="computedClass"
|
||||
>
|
||||
<a
|
||||
class="sub-menu-title"
|
||||
:class="getMenuItemClass"
|
||||
data-tooltip
|
||||
aria-haspopup="true"
|
||||
:title="menuItem.toolTip"
|
||||
>
|
||||
<i :class="menuItem.icon" />
|
||||
{{ menuItem.label }}
|
||||
<div class="wrap">
|
||||
<i :class="menuItem.icon" />
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</div>
|
||||
<span
|
||||
v-if="showItem(menuItem)"
|
||||
class="ion-ios-plus-outline"
|
||||
class="child-icon ion-android-add-circle"
|
||||
@click.prevent="newLinkClick"
|
||||
/>
|
||||
</a>
|
||||
|
@ -28,12 +31,14 @@
|
|||
:to="child.toState"
|
||||
>
|
||||
<a href="#">
|
||||
<i
|
||||
v-if="computedInboxClass(child)"
|
||||
class="inbox-icon"
|
||||
:class="computedInboxClass(child)"
|
||||
></i>
|
||||
{{ child.label }}
|
||||
<div class="wrap">
|
||||
<i
|
||||
v-if="computedInboxClass(child)"
|
||||
class="inbox-icon"
|
||||
:class="computedInboxClass(child)"
|
||||
></i>
|
||||
{{ child.label }}
|
||||
</div>
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
|
@ -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 {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sub-menu-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -80,7 +80,8 @@ export default {
|
|||
}
|
||||
}}
|
||||
>
|
||||
{`${this.name} (${this.getItemCount})`}
|
||||
{`${this.name}`}
|
||||
<span class="badge">{this.getItemCount}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
v-if="channel === 'website'"
|
||||
src="~dashboard/assets/images/channels/website.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'twilio'"
|
||||
src="~dashboard/assets/images/channels/twilio.png"
|
||||
/>
|
||||
<h3 class="channel__title">
|
||||
{{ channel }}
|
||||
</h3>
|
||||
|
@ -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)) {
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
v-else
|
||||
:username="username"
|
||||
:class="thumbnailClass"
|
||||
background-color="#1f93ff"
|
||||
color="white"
|
||||
:size="avatarSize"
|
||||
/>
|
||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
|||
currentChat: 'getSelectedChat',
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
|
||||
isActiveChat() {
|
||||
|
@ -96,7 +97,11 @@ export default {
|
|||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
const path = conversationUrl(activeInbox, chat.id);
|
||||
const path = conversationUrl(
|
||||
this.currentUser.account_id,
|
||||
activeInbox,
|
||||
chat.id
|
||||
);
|
||||
router.push({ path: frontendURL(path) });
|
||||
},
|
||||
extractMessageText(chatItem) {
|
||||
|
@ -111,7 +116,7 @@ export default {
|
|||
}
|
||||
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
||||
return `
|
||||
<i class="${this.$t(`${key}.ICON`)}"></i>
|
||||
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
|
||||
${this.$t(`${key}.CONTENT`)}
|
||||
`;
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{{ chat.meta.sender.name }}
|
||||
</h3>
|
||||
<button
|
||||
class="user--profile__button"
|
||||
class="user--profile__button clear button small"
|
||||
@click="$emit('contactPanelToggle')"
|
||||
>
|
||||
{{ viewProfileButtonLabel }}
|
||||
|
|
|
@ -1,27 +1,17 @@
|
|||
<template>
|
||||
<li v-if="data.attachment || data.content" :class="alignBubble">
|
||||
<div :class="wrapClass">
|
||||
<p
|
||||
v-tooltip.top-start="sentByMessage"
|
||||
:class="{ bubble: isBubble, 'is-private': isPrivate }"
|
||||
>
|
||||
<p v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-image
|
||||
v-if="data.attachment && data.attachment.file_type === 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-audio
|
||||
v-if="data.attachment && data.attachment.file_type === 'audio'"
|
||||
<bubble-file
|
||||
v-if="data.attachment && data.attachment.file_type !== 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-map
|
||||
v-if="data.attachment && data.attachment.file_type === 'location'"
|
||||
:lat="data.attachment.coordinates_lat"
|
||||
:lng="data.attachment.coordinates_long"
|
||||
:label="data.attachment.fallback_title"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
|
@ -36,25 +26,25 @@
|
|||
/>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <img v-if="showSenderData" src="https://chatwoot-staging.s3-us-west-2.amazonaws.com/uploads/avatar/contact/3415/thumb_10418362_10201264050880840_6087258728802054624_n.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAI3KBM2ES3VRHQHPQ%2F20170422%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20170422T075421Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=8d5ff60e41415515f59ff682b9a4e4c0574d9d9aabfeff1dc5a51087a9b49e03" class="sender--thumbnail"> -->
|
||||
<!-- <img
|
||||
src="https://randomuser.me/api/portraits/women/94.jpg"
|
||||
class="sender--thumbnail"
|
||||
/> -->
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-named-as-default */
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import getEmojiSVG from '../emoji/utils';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleMap from './bubble/Map';
|
||||
import BubbleAudio from './bubble/Audio';
|
||||
import BubbleFile from './bubble/File';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleText,
|
||||
BubbleImage,
|
||||
BubbleMap,
|
||||
BubbleAudio,
|
||||
BubbleFile,
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
props: {
|
||||
|
@ -81,6 +71,11 @@ export default {
|
|||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
},
|
||||
hasImageAttachment() {
|
||||
const { attachment = {} } = this.data;
|
||||
const { file_type: fileType } = attachment;
|
||||
return fileType === 'image';
|
||||
},
|
||||
isPrivate() {
|
||||
return this.data.private;
|
||||
},
|
||||
|
@ -102,9 +97,30 @@ export default {
|
|||
'activity-wrap': !this.isBubble,
|
||||
};
|
||||
},
|
||||
bubbleClass() {
|
||||
return {
|
||||
bubble: this.isBubble,
|
||||
'is-private': this.isPrivate,
|
||||
'is-image': this.hasImageAttachment,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getEmojiSVG,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables.scss';
|
||||
.wrap {
|
||||
.is-image {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 32rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,17 @@
|
|||
@click="onClick()"
|
||||
@blur="onBlur()"
|
||||
/>
|
||||
<file-upload
|
||||
v-if="showFileUpload"
|
||||
:size="4096 * 4096"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<i
|
||||
v-if="!isUploading.image"
|
||||
class="icon ion-android-attach attachment"
|
||||
/>
|
||||
<woot-spinner v-if="isUploading.image" />
|
||||
</file-upload>
|
||||
<i
|
||||
class="icon ion-happy-outline"
|
||||
:class="{ active: showEmojiPicker }"
|
||||
|
@ -77,6 +88,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import emojione from 'emojione';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
|
||||
import EmojiInput from '../emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
|
@ -85,6 +97,7 @@ export default {
|
|||
components: {
|
||||
EmojiInput,
|
||||
CannedResponse,
|
||||
FileUpload,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
|
@ -93,6 +106,11 @@ export default {
|
|||
isPrivate: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedResponsesList: false,
|
||||
isUploading: {
|
||||
audio: false,
|
||||
video: false,
|
||||
image: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -123,6 +141,9 @@ export default {
|
|||
}
|
||||
return 10000;
|
||||
},
|
||||
showFileUpload() {
|
||||
return this.channelType === 'Channel::WebWidget';
|
||||
},
|
||||
replyButtonLabel() {
|
||||
if (this.isPrivate) {
|
||||
return this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||
|
@ -180,21 +201,21 @@ export default {
|
|||
isEscape(e) {
|
||||
return e.keyCode === 27; // ESCAPE
|
||||
},
|
||||
sendMessage() {
|
||||
async sendMessage() {
|
||||
const isMessageEmpty = !this.message.replace(/\n/g, '').length;
|
||||
if (isMessageEmpty) {
|
||||
return;
|
||||
}
|
||||
if (isMessageEmpty) return;
|
||||
|
||||
if (!this.showCannedResponsesList) {
|
||||
this.$store
|
||||
.dispatch('sendMessage', {
|
||||
try {
|
||||
await this.$store.dispatch('sendMessage', {
|
||||
conversationId: this.currentChat.id,
|
||||
message: this.message,
|
||||
private: this.isPrivate,
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit('scrollToMessage');
|
||||
});
|
||||
this.$emit('scrollToMessage');
|
||||
} catch (error) {
|
||||
// Error
|
||||
}
|
||||
this.clearMessage();
|
||||
this.hideEmojiPicker();
|
||||
}
|
||||
|
@ -272,6 +293,20 @@ export default {
|
|||
: 'CONVERSATION.FOOTER.MSG_INPUT';
|
||||
return placeHolder;
|
||||
},
|
||||
|
||||
onFileUpload(file) {
|
||||
this.isUploading.image = true;
|
||||
this.$store
|
||||
.dispatch('sendAttachment', [this.currentChat.id, { file: file.file }])
|
||||
.then(() => {
|
||||
this.isUploading.image = false;
|
||||
this.$emit('scrollToMessage');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isUploading.image = false;
|
||||
this.$emit('scrollToMessage');
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="file message-text__wrap" @click="openLink">
|
||||
<div class="icon-wrap">
|
||||
<i class="ion-document-text"></i>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<h5 class="text-block-title">
|
||||
{{ decodeURI(fileName) }}
|
||||
</h5>
|
||||
<a
|
||||
class="download clear button small"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
:href="url"
|
||||
>
|
||||
{{ $t('CONVERSATION.DOWNLOAD') }}
|
||||
</a>
|
||||
</div>
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['url', 'readableTime'],
|
||||
computed: {
|
||||
fileName() {
|
||||
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||
return filename;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLink() {
|
||||
const win = window.open(this.url, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: $space-normal;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-wrap {
|
||||
font-size: $font-size-giga;
|
||||
color: $color-woot;
|
||||
line-height: 1;
|
||||
margin-left: $space-smaller;
|
||||
margin-right: $space-slab;
|
||||
}
|
||||
|
||||
.text-block-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
padding-right: $space-two;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,9 +5,18 @@ export const frontendURL = (path, params) => {
|
|||
return `/app/${path}${stringifiedParams}`;
|
||||
};
|
||||
|
||||
export const conversationUrl = (activeInbox, id) => {
|
||||
export const conversationUrl = (accountId, activeInbox, id) => {
|
||||
const path = activeInbox
|
||||
? `inbox/${activeInbox}/conversations/${id}`
|
||||
: `conversations/${id}`;
|
||||
? `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;
|
||||
};
|
||||
|
|
|
@ -1,20 +1,3 @@
|
|||
export const createWebsiteWidgetScript = websiteToken => `
|
||||
<script>
|
||||
(function(d,t) {
|
||||
var BASE_URL = '${window.location.origin}';
|
||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src= BASE_URL + "/packs/js/sdk.js";
|
||||
s.parentNode.insertBefore(g,s);
|
||||
g.onload=function(){
|
||||
window.chatwootSDK.run({
|
||||
websiteToken: '${websiteToken}',
|
||||
baseUrl: BASE_URL
|
||||
})
|
||||
}
|
||||
})(document,"script");
|
||||
</script>
|
||||
`;
|
||||
|
||||
export const createMessengerScript = pageId => `
|
||||
<script>
|
||||
window.fbAsyncInit = function() {
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import { frontendURL, conversationUrl } from '../URLHelper';
|
||||
import {
|
||||
frontendURL,
|
||||
conversationUrl,
|
||||
accountIdFromPathname,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
describe('conversationUrl', () => {
|
||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||
expect(conversationUrl(undefined, 1)).toBe('conversations/1');
|
||||
expect(conversationUrl(1, undefined, 1)).toBe(
|
||||
'accounts/1/conversations/1'
|
||||
);
|
||||
});
|
||||
it('should return ibox conversation URL if activeInbox is not nil', () => {
|
||||
expect(conversationUrl(2, 1)).toBe('inbox/2/conversations/1');
|
||||
expect(conversationUrl(1, 2, 1)).toBe(
|
||||
'accounts/1/inbox/2/conversations/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -18,4 +26,26 @@ describe('#URL Helpers', () => {
|
|||
expect(frontendURL('main', { ping: 'pong' })).toBe('/app/main?ping=pong');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
export const accountIdFromPathname = pathname => {
|
||||
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
|
||||
const accountId = isInsideAccountScopedURLs ? pathname.split('/')[3] : '';
|
||||
return Number(accountId);
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
describe('accountIdFromPathname', () => {
|
||||
it('should return account id if accont scoped url is passed', () => {
|
||||
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);
|
||||
});
|
||||
it('should return empty string if accont scoped url not is passed', () => {
|
||||
expect(accountIdFromPathname('/app/accounts/settings/general')).toBe('');
|
||||
});
|
||||
it('should return empty string if empty string is passed', () => {
|
||||
expect(accountIdFromPathname('')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
37
app/javascript/dashboard/i18n/de.js
Normal file
37
app/javascript/dashboard/i18n/de.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import de from './locale/de';
|
||||
|
||||
export default {
|
||||
...de,
|
||||
APP_GLOBAL: {
|
||||
TRIAL_MESSAGE: 'verbleibende Tage Probezeit.',
|
||||
TRAIL_BUTTON: 'Kaufe jetzt',
|
||||
},
|
||||
COMPONENTS: {
|
||||
CODE: {
|
||||
BUTTON_TEXT: 'Kopieren',
|
||||
COPY_SUCCESSFUL: 'Code erfolgreich in die Zwischenablage kopiert',
|
||||
},
|
||||
FILE_BUBBLE: {
|
||||
DOWNLOAD: 'Herunterladen',
|
||||
UPLOADING: 'Hochladen...',
|
||||
},
|
||||
},
|
||||
CONFIRM_EMAIL: 'Überprüfen...',
|
||||
SETTINGS: {
|
||||
INBOXES: {
|
||||
NEW_INBOX: 'Posteingang hinzufügen',
|
||||
},
|
||||
},
|
||||
SIDEBAR: {
|
||||
CONVERSATIONS: 'Gespräche',
|
||||
REPORTS: 'Berichte',
|
||||
SETTINGS: 'Die Einstellungen',
|
||||
HOME: 'Zuhause',
|
||||
AGENTS: 'Agenten',
|
||||
INBOXES: 'Posteingänge',
|
||||
CANNED_RESPONSES: 'Vorgefertigte Antworten',
|
||||
BILLING: 'Abrechnung',
|
||||
INTEGRATIONS: 'Integrationen',
|
||||
ACCOUNT_SETTINGS: 'Kontoeinstellungen',
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue