Merge branch 'release/1.6.0'

This commit is contained in:
Sojan 2020-07-08 01:00:45 +05:30
commit d03fad3e1d
827 changed files with 26325 additions and 3033 deletions

View file

@ -16,7 +16,7 @@ defaults: &defaults
- image: circleci/redis:alpine - image: circleci/redis:alpine
environment: environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f - CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false
jobs: jobs:
build: build:
<<: *defaults <<: *defaults
@ -69,11 +69,11 @@ jobs:
- run: - run:
name: Download cc-test-reporter name: Download cc-test-reporter
command: | command: |
mkdir -p tmp/ mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ./tmp/cc-test-reporter chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace: - persist_to_workspace:
root: tmp root: ~/tmp
paths: paths:
- cc-test-reporter - cc-test-reporter
@ -98,10 +98,10 @@ jobs:
- run: - run:
name: Run backend tests name: Run backend tests
command: | command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json ~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace: - persist_to_workspace:
root: tmp root: ~/tmp
paths: paths:
- codeclimate.backend.json - codeclimate.backend.json
@ -109,21 +109,23 @@ jobs:
name: Run frontend tests name: Run frontend tests
command: | command: |
yarn test:coverage yarn test:coverage
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info ~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
- persist_to_workspace: - persist_to_workspace:
root: tmp root: ~/tmp
paths: paths:
- codeclimate.frontend.json - codeclimate.frontend.json
# collect reports # collect reports
- store_test_results: - store_test_results:
path: /tmp/test-results path: ~/tmp/test-results
- store_artifacts: - store_artifacts:
path: /tmp/test-results path: ~/tmp/test-results
destination: test-results destination: test-results
- store_artifacts:
path: log
- run: - run:
name: Upload coverage results to Code Climate name: Upload coverage results to Code Climate
command: | command: |
./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json ~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json ~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json

View file

@ -59,6 +59,7 @@ MANDRILL_INGRESS_API_KEY=
ACTIVE_STORAGE_SERVICE=local ACTIVE_STORAGE_SERVICE=local
# Amazon S3 # Amazon S3
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
S3_BUCKET_NAME= S3_BUCKET_NAME=
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
@ -74,34 +75,35 @@ LOG_LEVEL=info
LOG_SIZE=500 LOG_SIZE=500
### This environment variables are only required if you are setting up social media channels ### This environment variables are only required if you are setting up social media channels
#facebook
# Facebook
# documentation: https://www.chatwoot.com/docs/facebook-setup
FB_VERIFY_TOKEN= FB_VERIFY_TOKEN=
FB_APP_SECRET= FB_APP_SECRET=
FB_APP_ID= FB_APP_ID=
# Twitter # Twitter
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
TWITTER_APP_ID= TWITTER_APP_ID=
TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET= TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT= TWITTER_ENVIRONMENT=
#slack integration
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
### Change this env variable only if you are using a custom build mobile app ### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables ## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app IOS_APP_ID=6C953F3RX2.com.chatwoot.app
#### This environment variables are only required in hosted version which has billing
ENABLE_BILLING=
## chargebee settings
CHARGEBEE_API_KEY=
CHARGEBEE_SITE=
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
## Push Notification ## Push Notification
## generate a new key value here : https://d3v.one/vapid-key-generator/ ## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY= # VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY= # VAPID_PRIVATE_KEY=
#
# for mobile apps
# FCM_SERVER_KEY=
## Bot Customizations ## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true USE_INBOX_AVATAR_FOR_BOT=true

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
open_collective: chatwoot open_collective: chatwoot
github: chatwoot

View file

@ -8,8 +8,17 @@ Lint/RaiseException:
Enabled: true Enabled: true
Lint/StructNewOverride: Lint/StructNewOverride:
Enabled: true Enabled: true
Lint/DeprecatedOpenSSLConstant:
Enabled: true
Lint/MixedRegexpCaptureTypes:
Enabled: true
Layout/LineLength: Layout/LineLength:
Max: 150 Max: 150
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Metrics/ClassLength: Metrics/ClassLength:
Max: 125 Max: 125
Exclude: Exclude:
@ -18,6 +27,8 @@ RSpec/ExampleLength:
Max: 25 Max: 25
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Style/ExponentialNotation:
Enabled: false
Style/FrozenStringLiteralComment: Style/FrozenStringLiteralComment:
Enabled: false Enabled: false
Style/SymbolArray: Style/SymbolArray:
@ -28,6 +39,14 @@ Style/HashTransformKeys:
Enabled: true Enabled: true
Style/HashTransformValues: Style/HashTransformValues:
Enabled: true Enabled: true
Style/RedundantFetchBlock:
Enabled: true
Style/RedundantRegexpCharacterClass:
Enabled: true
Style/RedundantRegexpEscape:
Enabled: true
Style/SlicingWithRange:
Enabled: true
Style/GlobalVars: Style/GlobalVars:
Exclude: Exclude:
- 'config/initializers/redis.rb' - 'config/initializers/redis.rb'
@ -65,7 +84,6 @@ Style/GuardClause:
- 'app/builders/account_builder.rb' - 'app/builders/account_builder.rb'
- 'app/models/attachment.rb' - 'app/models/attachment.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
- 'lib/webhooks/chargebee.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize: Metrics/AbcSize:
Exclude: Exclude:
@ -103,8 +121,8 @@ AllCops:
Exclude: Exclude:
- 'bin/**/*' - 'bin/**/*'
- 'db/schema.rb' - 'db/schema.rb'
- 'config/**/*'
- 'public/**/*' - 'public/**/*'
- 'config/initializers/bot.rb'
- 'vendor/**/*' - 'vendor/**/*'
- 'node_modules/**/*' - 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake' - 'lib/tasks/auto_annotate_models.rake'

View file

@ -88,7 +88,6 @@ Naming/MemoizedInstanceVariableName:
- 'app/controllers/application_controller.rb' - 'app/controllers/application_controller.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
- 'lib/integrations/widget/outgoing_message_builder.rb' - 'lib/integrations/widget/outgoing_message_builder.rb'
- 'lib/webhooks/chargebee.rb'
# Offense count: 4 # Offense count: 4
# Cop supports --auto-correct. # Cop supports --auto-correct.
@ -187,7 +186,6 @@ Rails/EnumHash:
- 'app/models/attachment.rb' - 'app/models/attachment.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/models/subscription.rb'
- 'app/models/user.rb' - 'app/models/user.rb'
# Offense count: 1 # Offense count: 1
@ -226,7 +224,6 @@ Rails/Output:
Rails/TimeZone: Rails/TimeZone:
Exclude: Exclude:
- 'app/builders/report_builder.rb' - 'app/builders/report_builder.rb'
- 'app/models/subscription.rb'
- 'lib/reports/update_account_identity.rb' - 'lib/reports/update_account_identity.rb'
- 'lib/reports/update_agent_identity.rb' - 'lib/reports/update_agent_identity.rb'
- 'lib/reports/update_identity.rb' - 'lib/reports/update_identity.rb'
@ -269,24 +266,6 @@ Style/CommentedKeyword:
- 'app/controllers/api/v1/conversations/labels_controller.rb' - 'app/controllers/api/v1/conversations/labels_controller.rb'
- 'app/controllers/api/v1/labels_controller.rb' - 'app/controllers/api/v1/labels_controller.rb'
# Offense count: 1
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'lib/constants/redis_keys.rb'
# Offense count: 4
# Configuration parameters: AllowedVariables.
Style/GlobalVars:
Exclude:
- 'lib/redis/alfred.rb'
# Offense count: 4
Style/IdenticalConditionalBranches:
Exclude:
- 'app/controllers/api/v1/reports_controller.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: AllowIfModifier. # Configuration parameters: AllowIfModifier.
Style/IfInsideElse: Style/IfInsideElse:

View file

@ -51,7 +51,7 @@ linters:
ElsePlacement: ElsePlacement:
enabled: true enabled: true
style: same_line # or 'new_line' style: new_line
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
enabled: true enabled: true

View file

@ -56,9 +56,6 @@ gem 'administrate'
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
gem 'wisper', '2.0.0' gem 'wisper', '2.0.0'
##--- gems for billing ---##
gem 'chargebee'
##--- gems for channels ---## ##--- gems for channels ---##
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'telegram-bot-ruby' gem 'telegram-bot-ruby'
@ -68,6 +65,8 @@ gem 'twilio-ruby', '~> 5.32.0'
gem 'twitty' gem 'twitty'
# facebook client # facebook client
gem 'koala' gem 'koala'
# slack client
gem 'slack-ruby-client'
# Random name generator # Random name generator
gem 'haikunator' gem 'haikunator'
@ -84,6 +83,7 @@ gem 'sidekiq'
gem 'flag_shih_tzu' gem 'flag_shih_tzu'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm'
gem 'webpush' gem 'webpush'
group :development do group :development do
@ -117,4 +117,5 @@ group :development, :test do
gem 'simplecov', '0.17.1', require: false gem 'simplecov', '0.17.1', require: false
gem 'spring' gem 'spring'
gem 'spring-watcher-listen' gem 'spring-watcher-listen'
gem 'webmock'
end end

View file

@ -18,56 +18,56 @@ GEM
specs: specs:
action-cable-testing (0.6.1) action-cable-testing (0.6.1)
actioncable (>= 5.0) actioncable (>= 5.0)
actioncable (6.0.3.1) actioncable (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.1) actionmailbox (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
activejob (= 6.0.3.1) activejob (= 6.0.3.2)
activerecord (= 6.0.3.1) activerecord (= 6.0.3.2)
activestorage (= 6.0.3.1) activestorage (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.0.3.1) actionmailer (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
actionview (= 6.0.3.1) actionview (= 6.0.3.2)
activejob (= 6.0.3.1) activejob (= 6.0.3.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.0.3.1) actionpack (6.0.3.2)
actionview (= 6.0.3.1) actionview (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
rack (~> 2.0, >= 2.0.8) rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.1) actiontext (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
activerecord (= 6.0.3.1) activerecord (= 6.0.3.2)
activestorage (= 6.0.3.1) activestorage (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.0.3.1) actionview (6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.1) activejob (6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.0.3.1) activemodel (6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
activerecord (6.0.3.1) activerecord (6.0.3.2)
activemodel (= 6.0.3.1) activemodel (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
activestorage (6.0.3.1) activestorage (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
activejob (= 6.0.3.1) activejob (= 6.0.3.2)
activerecord (= 6.0.3.1) activerecord (= 6.0.3.2)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (6.0.3.1) activesupport (6.0.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -91,26 +91,26 @@ GEM
annotate (3.1.1) annotate (3.1.1)
activerecord (>= 3.2, < 7.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.0) ast (2.4.1)
attr_extras (6.2.3) attr_extras (6.2.4)
autoprefixer-rails (9.7.6) autoprefixer-rails (9.8.2)
execjs execjs
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.317.0) aws-partitions (1.332.0)
aws-sdk-core (3.96.1) aws-sdk-core (3.100.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.31.0) aws-sdk-kms (1.34.1)
aws-sdk-core (~> 3, >= 3.71.0) aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.65.0) aws-sdk-s3 (1.69.1)
aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-core (~> 3, >= 3.99.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.3) aws-sigv4 (1.2.0)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
axiom-types (0.1.1) axiom-types (0.1.1)
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
@ -127,25 +127,24 @@ GEM
bootsnap (1.4.6) bootsnap (1.4.6)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.8.2) brakeman (4.8.2)
browser (4.1.0) browser (4.2.0)
builder (3.2.4) builder (3.2.4)
bullet (6.1.0) bullet (6.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
bundler-audit bundler-audit
bundler-audit (0.6.1) bundler-audit (0.7.0.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (>= 0.18, < 2)
byebug (11.1.3) byebug (11.1.3)
chargebee (2.7.5) coderay (1.1.3)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
coderay (1.1.2)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.6) concurrent-ruby (1.1.6)
connection_pool (2.2.2) connection_pool (2.2.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.6) crass (1.0.6)
datetime_picker_rails (0.0.7) datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1) momentjs-rails (>= 2.8.1)
@ -153,17 +152,18 @@ GEM
declarative-option (0.1.0) declarative-option (0.1.0)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
devise (4.7.1) devise (4.7.2)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise_token_auth (1.1.3) devise_token_auth (1.1.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
devise (> 3.5.2, < 5) devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1) rails (>= 4.2.0, < 6.1)
diff-lcs (1.3) sprockets (= 3.7.2)
diff-lcs (1.4)
digest-crc (0.5.1) digest-crc (0.5.1)
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20190701) domain_name (0.5.20190701)
@ -178,23 +178,26 @@ GEM
facebook-messenger (1.5.0) facebook-messenger (1.5.0)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5) rack (>= 1.4.5)
factory_bot (5.2.0) factory_bot (6.0.2)
activesupport (>= 4.2.0) activesupport (>= 5.0.0)
factory_bot_rails (5.2.0) factory_bot_rails (6.0.0)
factory_bot (~> 5.2.0) factory_bot (~> 6.0.0)
railties (>= 4.2.0) railties (>= 5.0.0)
faker (2.11.0) faker (2.12.0)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.0.1) faraday (1.0.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (1.0.0) faraday_middleware (1.0.0)
faraday (~> 1.0) faraday (~> 1.0)
ffi (1.12.2) fcm (1.0.1)
faraday (~> 1.0.0)
ffi (1.13.1)
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.1) foreman (0.87.1)
gli (2.19.1)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
google-api-client (0.39.4) google-api-client (0.41.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9) googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.0)
@ -205,17 +208,17 @@ GEM
google-cloud-core (1.5.0) google-cloud-core (1.5.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.3.1) google-cloud-env (1.3.2)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0) google-cloud-errors (1.0.1)
google-cloud-storage (1.26.1) google-cloud-storage (1.26.2)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-api-client (~> 0.33) google-api-client (~> 0.33)
google-cloud-core (~> 1.2) google-cloud-core (~> 1.2)
googleauth (~> 0.9) googleauth (~> 0.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.12.0) googleauth (0.13.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -226,16 +229,17 @@ GEM
activesupport (>= 5) activesupport (>= 5)
haikunator (1.1.0) haikunator (1.1.0)
hana (1.3.6) hana (1.3.6)
hashdiff (1.0.1)
hashie (4.1.0) hashie (4.1.0)
hkdf (0.3.0) hkdf (0.3.0)
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
httparty (0.18.0) httparty (0.18.1)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.8.2) i18n (1.8.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ice_nine (0.11.2) ice_nine (0.11.2)
inflecto (0.0.2) inflecto (0.0.2)
@ -247,7 +251,6 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.3.0) json (2.3.0)
json_pure (2.3.0)
jwt (2.2.1) jwt (2.2.1)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
@ -272,7 +275,7 @@ GEM
listen (3.2.1) listen (3.2.1)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.5.0) loofah (2.6.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -302,9 +305,9 @@ GEM
oauth (0.5.4) oauth (0.5.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.0) os (1.1.0)
parallel (1.19.1) parallel (1.19.2)
parser (2.7.1.2) parser (2.7.1.4)
ast (~> 2.4.0) ast (~> 2.4.1)
pg (1.2.3) pg (1.2.3)
pry (0.13.1) pry (0.13.1)
coderay (~> 1.1) coderay (~> 1.1)
@ -316,8 +319,8 @@ GEM
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (2.2.2) rack (2.2.3)
rack-cache (1.11.1) rack-cache (1.12.0)
rack (>= 0.4) rack (>= 0.4)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
@ -327,29 +330,29 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.0.3.1) rails (6.0.3.2)
actioncable (= 6.0.3.1) actioncable (= 6.0.3.2)
actionmailbox (= 6.0.3.1) actionmailbox (= 6.0.3.2)
actionmailer (= 6.0.3.1) actionmailer (= 6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
actiontext (= 6.0.3.1) actiontext (= 6.0.3.2)
actionview (= 6.0.3.1) actionview (= 6.0.3.2)
activejob (= 6.0.3.1) activejob (= 6.0.3.2)
activemodel (= 6.0.3.1) activemodel (= 6.0.3.2)
activerecord (= 6.0.3.1) activerecord (= 6.0.3.2)
activestorage (= 6.0.3.1) activestorage (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 6.0.3.1) railties (= 6.0.3.2)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0) rails-html-sanitizer (1.3.0)
loofah (~> 2.3) loofah (~> 2.3)
railties (6.0.3.1) railties (6.0.3.2)
actionpack (= 6.0.3.1) actionpack (= 6.0.3.2)
activesupport (= 6.0.3.1) activesupport (= 6.0.3.2)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0) thor (>= 0.20.3, < 2.0)
@ -358,19 +361,20 @@ GEM
rb-fsevent (0.10.4) rb-fsevent (0.10.4)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (4.1.4) redis (4.2.1)
redis-namespace (1.7.0) redis-namespace (1.7.0)
redis (>= 3.0.4) redis (>= 3.0.4)
redis-rack-cache (2.2.1) redis-rack-cache (2.2.1)
rack-cache (>= 1.10, < 2) rack-cache (>= 1.10, < 2)
redis-store (>= 1.6, < 2) redis-store (>= 1.6, < 2)
redis-store (1.8.2) redis-store (1.9.0)
redis (>= 4, < 5) redis (>= 4, < 5)
regexp_parser (1.7.1)
representable (3.0.4) representable (3.0.4)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
responders (3.0.0) responders (3.0.1)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
rest-client (2.1.0) rest-client (2.1.0)
@ -397,28 +401,33 @@ GEM
rspec-mocks (~> 3.9) rspec-mocks (~> 3.9)
rspec-support (~> 3.9) rspec-support (~> 3.9)
rspec-support (3.9.3) rspec-support (3.9.3)
rubocop (0.83.0) rubocop (0.86.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.7.0.1) parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml rexml
rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0) unicode-display_width (>= 1.4.0, < 2.0)
rubocop-performance (1.5.2) rubocop-ast (0.0.3)
parser (>= 2.7.0.1)
rubocop-performance (1.6.1)
rubocop (>= 0.71.0) rubocop (>= 0.71.0)
rubocop-rails (2.5.2) rubocop-rails (2.6.0)
activesupport activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.72.0) rubocop (>= 0.82.0)
rubocop-rspec (1.39.0) rubocop-rspec (1.40.0)
rubocop (>= 0.68.1) rubocop (>= 0.68.1)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
safe_yaml (1.0.5)
sass (3.7.4) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.3.0) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
sassc-rails (2.1.2) sassc-rails (2.1.2)
railties (>= 4.0.0) railties (>= 4.0.0)
@ -454,11 +463,18 @@ GEM
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.2) simplecov-html (0.10.2)
slack-ruby-client (0.14.6)
activesupport
faraday (>= 0.9)
faraday_middleware
gli
hashie
websocket-driver
spring (2.1.0) spring (2.1.0)
spring-watcher-listen (2.0.1) spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 1.2, < 3.0)
sprockets (4.0.0) sprockets (3.7.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.1) sprockets-rails (3.2.1)
@ -470,7 +486,7 @@ GEM
inflecto inflecto
virtus virtus
telephone_number (1.4.7) telephone_number (1.4.7)
thor (0.20.3) thor (1.0.1)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.10) tilt (2.0.10)
time_diff (0.3.0) time_diff (0.3.0)
@ -504,11 +520,15 @@ GEM
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
warden (1.2.8) warden (1.2.8)
rack (>= 2.0.6) rack (>= 2.0.6)
web-console (4.0.2) web-console (4.0.3)
actionview (>= 6.0.0) actionview (>= 6.0.0)
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.1.1) webpacker (5.1.1)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
@ -517,9 +537,9 @@ GEM
webpush (1.0.0) webpush (1.0.0)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.1) websocket-driver (0.7.2)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4) websocket-extensions (0.1.5)
wisper (2.0.0) wisper (2.0.0)
zeitwerk (2.3.0) zeitwerk (2.3.0)
@ -540,13 +560,13 @@ DEPENDENCIES
bullet bullet
bundle-audit bundle-audit
byebug byebug
chargebee
devise devise
devise_token_auth devise_token_auth
dotenv-rails dotenv-rails
facebook-messenger facebook-messenger
factory_bot_rails factory_bot_rails
faker faker
fcm
flag_shih_tzu flag_shih_tzu
foreman foreman
google-cloud-storage google-cloud-storage
@ -585,6 +605,7 @@ DEPENDENCIES
shoulda-matchers shoulda-matchers
sidekiq sidekiq
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client
spring spring
spring-watcher-listen spring-watcher-listen
telegram-bot-ruby telegram-bot-ruby
@ -596,6 +617,7 @@ DEPENDENCIES
uglifier uglifier
valid_email2 valid_email2
web-console web-console
webmock
webpacker webpacker
webpush webpush
wisper (= 2.0.0) wisper (= 2.0.0)

View file

@ -29,7 +29,7 @@ class ContactMergeAction
end end
def merge_messages def merge_messages
Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id) Message.where(sender: @mergee_contact).update(sender: @base_contact)
end end
def merge_contact_inboxes def merge_contact_inboxes

View file

@ -117,7 +117,8 @@ class Messages::MessageBuilder
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: @message_type, message_type: @message_type,
content: response.content, content: response.content,
source_id: response.identifier source_id: response.identifier,
sender: contact
} }
end end

View file

@ -37,7 +37,7 @@ class Messages::Outgoing::NormalBuilder
message_type: :outgoing, message_type: :outgoing,
content: @content, content: @content,
private: @private, private: @private,
user_id: @user&.id, sender: @user,
source_id: @fb_id, source_id: @fb_id,
content_type: @content_type, content_type: @content_type,
items: @items items: @items

View file

@ -12,6 +12,8 @@ class NotificationSubscriptionBuilder
def identifier def identifier
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push' @identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
@identifier ||= params[:subscription_attributes][:device_id] if params[:subscription_type] == 'fcm'
@identifier
end end
def identifier_subscription def identifier_subscription

View file

@ -1,10 +1,47 @@
class RoomChannel < ApplicationCable::Channel class RoomChannel < ApplicationCable::Channel
def subscribed def subscribed
stream_from params[:pubsub_token] ensure_stream
::OnlineStatusTracker.add_subscription(params[:pubsub_token]) current_user
current_account
update_subscription
broadcast_presence
end end
def unsubscribed def update_presence
::OnlineStatusTracker.remove_subscription(params[:pubsub_token]) update_subscription
broadcast_presence
end
private
def broadcast_presence
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
end
def ensure_stream
@pubsub_token = params[:pubsub_token]
stream_from @pubsub_token
end
def update_subscription
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
end
def current_user
@current_user ||= if params[:user_id].blank?
Contact.find_by!(pubsub_token: @pubsub_token)
else
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
end
end
def current_account
@current_account ||= if @current_user.is_a? Contact
@current_user.account
else
@current_user.accounts.find(params[:account_id])
end
end end
end end

View file

@ -10,12 +10,4 @@ class Api::BaseController < ApplicationController
def authenticate_by_access_token? def authenticate_by_access_token?
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present? request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
end end
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
def check_billing_enabled
raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED']
end
end end

View file

@ -1,10 +1,10 @@
class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::BaseController
before_action :set_base_contact, only: [:create] before_action :set_base_contact, only: [:create]
before_action :set_mergee_contact, only: [:create] before_action :set_mergee_contact, only: [:create]
def create def create
contact_merge_action = ContactMergeAction.new( contact_merge_action = ContactMergeAction.new(
account: current_account, account: Current.account,
base_contact: @base_contact, base_contact: @base_contact,
mergee_contact: @mergee_contact mergee_contact: @mergee_contact
) )
@ -23,6 +23,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController
end end
def contacts def contacts
@contacts ||= current_account.contacts @contacts ||= Current.account.contacts
end end
end end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::AgentsController < Api::BaseController class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
before_action :fetch_agent, except: [:create, :index] before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization before_action :check_authorization
before_action :find_user, only: [:create] before_action :find_user, only: [:create]
@ -46,7 +46,7 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
def save_account_user def save_account_user
AccountUser.create!( AccountUser.create!(
account_id: current_account.id, account_id: Current.account.id,
user_id: @user.id, user_id: @user.id,
role: new_agent_params[:role], role: new_agent_params[:role],
inviter_id: current_user.id inviter_id: current_user.id
@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::BaseController
end end
def agents def agents
@agents ||= current_account.users @agents ||= Current.account.users
end end
end end

View file

@ -0,0 +1,31 @@
class Api::V1::Accounts::BaseController < Api::BaseController
before_action :current_account
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_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
switch_locale account
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
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
end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::CallbacksController < Api::BaseController class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
before_action :inbox, only: [:reauthorize_page] before_action :inbox, only: [:reauthorize_page]
def register_facebook_page def register_facebook_page
@ -7,11 +7,11 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
page_id = params[:page_id] page_id = params[:page_id]
inbox_name = params[:inbox_name] inbox_name = params[:inbox_name]
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
facebook_channel = current_account.facebook_pages.create!( facebook_channel = Current.account.facebook_pages.create!(
page_id: page_id, user_access_token: user_access_token, page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token page_access_token: page_access_token
) )
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel) @facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
set_avatar(@facebook_inbox, page_id) set_avatar(@facebook_inbox, page_id)
rescue StandardError => e rescue StandardError => e
Rails.logger.info e Rails.logger.info e
@ -22,7 +22,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts')) @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
end end
# get params[:inbox_id], current_account, params[:omniauth_token] # get params[:inbox_id], current_account. params[:omniauth_token]
def reauthorize_page def reauthorize_page
if @inbox&.facebook? if @inbox&.facebook?
fb_page_id = @inbox.channel.page_id fb_page_id = @inbox.channel.page_id
@ -40,7 +40,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
private private
def inbox def inbox
@inbox = current_account.inboxes.find_by(id: params[:inbox_id]) @inbox = Current.account.inboxes.find_by(id: params[:inbox_id])
end end
def update_fb_page(fb_page_id, access_token) def update_fb_page(fb_page_id, access_token)
@ -50,7 +50,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
end end
def get_fb_page(fb_page_id) def get_fb_page(fb_page_id)
current_account.facebook_pages.find_by(page_id: fb_page_id) Current.account.facebook_pages.find_by(page_id: fb_page_id)
end end
def fb_object def fb_object
@ -69,7 +69,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
return [] if data.empty? return [] if data.empty?
data.inject([]) do |result, page_detail| data.inject([]) do |result, page_detail|
page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false
result << page_detail result << page_detail
end end
end end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::CannedResponsesController < Api::BaseController class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseController
before_action :fetch_canned_response, only: [:update, :destroy] before_action :fetch_canned_response, only: [:update, :destroy]
def index def index
@ -6,7 +6,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
end end
def create def create
@canned_response = current_account.canned_responses.new(canned_response_params) @canned_response = Current.account.canned_responses.new(canned_response_params)
@canned_response.save! @canned_response.save!
render json: @canned_response render json: @canned_response
end end
@ -24,7 +24,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
private private
def fetch_canned_response def fetch_canned_response
@canned_response = current_account.canned_responses.find(params[:id]) @canned_response = Current.account.canned_responses.find(params[:id])
end end
def canned_response_params def canned_response_params
@ -33,9 +33,9 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController
def canned_responses def canned_responses
if params[:search] if params[:search]
current_account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%") Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%")
else else
current_account.canned_responses Current.account.canned_responses
end end
end end
end end

View file

@ -1,5 +1,4 @@
class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts::BaseController
before_action :current_account
before_action :authorize_request before_action :authorize_request
def create def create
@ -38,13 +37,13 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseControlle
end end
def build_inbox def build_inbox
@twilio_channel = current_account.twilio_sms.create!( @twilio_channel = Current.account.twilio_sms.create!(
account_sid: permitted_params[:account_sid], account_sid: permitted_params[:account_sid],
auth_token: permitted_params[:auth_token], auth_token: permitted_params[:auth_token],
phone_number: phone_number, phone_number: phone_number,
medium: medium medium: medium
) )
@inbox = current_account.inboxes.create( @inbox = Current.account.inboxes.create(
name: permitted_params[:name], name: permitted_params[:name],
channel: @twilio_channel channel: @twilio_channel
) )

View file

@ -1,6 +1,6 @@
class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController
def index def index
@conversations = current_account.conversations.includes( @conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox :assignee, :contact, :inbox
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id]) ).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
end end
@ -9,7 +9,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController
def inbox_ids def inbox_ids
if current_user.administrator? if current_user.administrator?
current_account.inboxes.pluck(:id) Current.account.inboxes.pluck(:id)
elsif current_user.agent? elsif current_user.agent?
current_user.assigned_inboxes.pluck(:id) current_user.assigned_inboxes.pluck(:id)
else else

View file

@ -1,17 +1,17 @@
class Api::V1::Accounts::ContactsController < Api::BaseController class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
before_action :check_authorization before_action :check_authorization
before_action :fetch_contact, only: [:show, :update] before_action :fetch_contact, only: [:show, :update]
def index def index
@contacts = current_account.contacts @contacts = Current.account.contacts
end end
def show; end def show; end
def create def create
@contact = Contact.new(contact_create_params) @contact = Current.account.contacts.new(contact_create_params)
@contact.save! @contact.save!
render json: @contact render json: @contact
end end
@ -31,10 +31,10 @@ class Api::V1::Accounts::ContactsController < Api::BaseController
end end
def fetch_contact def fetch_contact
@contact = current_account.contacts.find(params[:id]) @contact = Current.account.contacts.find(params[:id])
end end
def contact_create_params def contact_create_params
params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex) params.require(:contact).permit(:name, :email, :phone_number)
end end
end end

View file

@ -1,10 +1,8 @@
class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
before_action :set_conversation, only: [:create]
# assign agent to a conversation # assign agent to a conversation
def create def create
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation # 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]) assignee = Current.account.users.find_by(id: params[:assignee_id])
@conversation.update_assignee(assignee) @conversation.update_assignee(assignee)
render json: assignee render json: assignee
end end

View file

@ -0,0 +1,9 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
before_action :conversation
private
def conversation
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
end
end

View file

@ -1,6 +1,4 @@
class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController
before_action :set_conversation, only: [:create, :index]
def create def create
@conversation.update_labels(params[:labels]) @conversation.update_labels(params[:labels])
@labels = @conversation.label_list @labels = @conversation.label_list

View file

@ -1,12 +1,11 @@
class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
before_action :set_conversation, only: [:index, :create]
def index def index
@messages = message_finder.perform @messages = message_finder.perform
end end
def create def create
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params) user = current_user || @resource
mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params)
@message = mb.perform @message = mb.perform
end end

View file

@ -1,6 +1,5 @@
class Api::V1::Accounts::ConversationsController < Api::BaseController class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
include Events::Types include Events::Types
before_action :current_account
before_action :conversation, except: [:index] before_action :conversation, except: [:index]
before_action :contact_inbox, only: [:create] before_action :contact_inbox, only: [:create]
@ -62,7 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
end end
def conversation def conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id]) @conversation ||= Current.account.conversations.find_by(display_id: params[:id])
end end
def contact_inbox def contact_inbox
@ -71,7 +70,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
def conversation_params def conversation_params
{ {
account_id: current_account.id, account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id, inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id, contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id contact_inbox_id: @contact_inbox.id

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController class Api::V1::Accounts::FacebookIndicatorsController < Api::V1::Accounts::BaseController
before_action :set_access_token before_action :set_access_token
around_action :handle_with_exception around_action :handle_with_exception
@ -38,7 +38,7 @@ class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController
end end
def inbox def inbox
@inbox ||= current_account.inboxes.find(permitted_params[:inbox_id]) @inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id])
end end
def set_access_token def set_access_token

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::InboxMembersController < Api::BaseController class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController
before_action :fetch_inbox, only: [:create, :show] before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create] before_action :current_agents_ids, only: [:create]
@ -12,7 +12,7 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController
end end
def show def show
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id)) @agents = Current.account.users.where(id: @inbox.members.pluck(:user_id))
end end
private private
@ -40,6 +40,6 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController
end end
def fetch_inbox def fetch_inbox
@inbox = current_account.inboxes.find(params[:inbox_id]) @inbox = Current.account.inboxes.find(params[:inbox_id])
end end
end end

View file

@ -1,17 +1,21 @@
class Api::V1::Accounts::InboxesController < Api::BaseController class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :current_account
before_action :fetch_inbox, except: [:index, :create] before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot] before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :check_authorization before_action :check_authorization
def index def index
@inboxes = policy_scope(current_account.inboxes) @inboxes = policy_scope(Current.account.inboxes)
end end
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget' 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 = Current.account.inboxes.build(
name: permitted_params[:name],
greeting_message: permitted_params[:greeting_message],
greeting_enabled: permitted_params[:greeting_enabled],
channel: channel
)
@inbox.avatar.attach(permitted_params[:avatar]) @inbox.avatar.attach(permitted_params[:avatar])
@inbox.save! @inbox.save!
end end
@ -41,7 +45,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
private private
def fetch_inbox def fetch_inbox
@inbox = current_account.inboxes.find(params[:id]) @inbox = Current.account.inboxes.find(params[:id])
end end
def fetch_agent_bot def fetch_agent_bot
@ -49,7 +53,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
end end
def web_widgets def web_widgets
current_account.web_widgets Current.account.web_widgets
end end
def check_authorization def check_authorization
@ -57,11 +61,12 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
end end
def permitted_params def permitted_params
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message]) params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline])
end end
def inbox_update_params def inbox_update_params
params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title, params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
:welcome_tagline, :agent_away_message]) channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline])
end end
end end

View file

@ -0,0 +1,18 @@
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
before_action :fetch_apps, only: [:index]
before_action :fetch_app, only: [:show]
def index; end
def show; end
private
def fetch_apps
@apps = Integrations::App.all.select(&:active?)
end
def fetch_app
@app = Integrations::App.find(id: params[:id])
end
end

View file

@ -0,0 +1,38 @@
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:update, :destroy]
def create
builder = Integrations::Slack::HookBuilder.new(
account: current_account,
code: params[:code],
inbox_id: params[:inbox_id]
)
@hook = builder.perform
create_chatwoot_slack_channel
end
def update
create_chatwoot_slack_channel
render json: @hook
end
def destroy
@hook.destroy
head :ok
end
private
def fetch_hook
@hook = Integrations::Hook.find_by(app_id: 'slack')
end
def create_chatwoot_slack_channel
channel = params[:channel] || 'customer-conversations'
builder = Integrations::Slack::ChannelBuilder.new(
hook: @hook, channel: channel
)
builder.perform
end
end

View file

@ -1,10 +1,38 @@
class Api::V1::Accounts::LabelsController < Api::BaseController class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
# list all labels in account before_action :current_account
before_action :fetch_label, except: [:index, :create]
before_action :check_authorization
def index def index
@labels = current_account.all_conversation_tags @labels = policy_scope(Current.account.labels)
end end
def most_used def show; end
@labels = ActsAsTaggableOn::Tag.most_used(params[:count] || 10)
def create
@label = Current.account.labels.create!(permitted_params)
end
def update
@label.update!(permitted_params)
end
def destroy
@label.destroy
head :ok
end
private
def fetch_label
@label = Current.account.labels.find(params[:id])
end
def check_authorization
authorize(Label)
end
def permitted_params
params.require(:label).permit(:title, :description, :color, :show_on_sidebar)
end end
end end

View file

@ -1,4 +1,4 @@
class Api::V1::Accounts::NotificationSettingsController < Api::BaseController class Api::V1::Accounts::NotificationSettingsController < Api::V1::Accounts::BaseController
before_action :set_user, :load_notification_setting before_action :set_user, :load_notification_setting
def show; end def show; end
@ -16,7 +16,7 @@ class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
end end
def load_notification_setting def load_notification_setting
@notification_setting = @user.notification_settings.find_by(account_id: current_account.id) @notification_setting = @user.notification_settings.find_by(account_id: Current.account.id)
end end
def notification_setting_params def notification_setting_params

View file

@ -1,11 +1,23 @@
class Api::V1::Accounts::NotificationsController < Api::BaseController class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
before_action :fetch_notification, only: [:update] before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all]
def index def index
@notifications = current_user.notifications.where(account_id: current_account.id) @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @notifications @notifications = current_user.notifications.where(account_id: current_account.id).page params[:page]
end
def read_all
if @primary_actor
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
.update(read_at: DateTime.now.utc)
else
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
end
head :ok
end end
def update def update
@ -15,6 +27,13 @@ class Api::V1::Accounts::NotificationsController < Api::BaseController
private private
def set_primary_actor
return unless params[:primary_actor_type]
return unless Notification::PRIMARY_ACTORS.include?(params[:primary_actor_type])
@primary_actor = params[:primary_actor_type].safe_constantize.find_by(id: params[:primary_actor_id])
end
def fetch_notification def fetch_notification
@notification = current_user.notifications.find(params[:id]) @notification = current_user.notifications.find(params[:id])
end end

View file

@ -1,13 +0,0 @@
class Api::V1::Accounts::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
def index
render json: current_account.subscription_data
end
def status
render json: current_account.subscription.summary
end
end

View file

@ -1,14 +1,13 @@
class Api::V1::Accounts::WebhooksController < Api::BaseController class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
before_action :current_account
before_action :check_authorization before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy] before_action :fetch_webhook, only: [:update, :destroy]
def index def index
@webhooks = current_account.webhooks @webhooks = Current.account.webhooks
end end
def create def create
@webhook = current_account.webhooks.new(webhook_params) @webhook = Current.account.webhooks.new(webhook_params)
@webhook.save! @webhook.save!
end end
@ -28,7 +27,7 @@ class Api::V1::Accounts::WebhooksController < Api::BaseController
end end
def fetch_webhook def fetch_webhook
@webhook = current_account.webhooks.find(params[:id]) @webhook = Current.account.webhooks.find(params[:id])
end end
def check_authorization def check_authorization

View file

@ -1,8 +1,8 @@
class Api::V1::Accounts::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
include AuthHelper include AuthHelper
skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
only: [:create], raise: false only: [:create], raise: false
before_action :check_signup_enabled, only: [:create] before_action :check_signup_enabled, only: [:create]
before_action :fetch_account, except: [:create] before_action :fetch_account, except: [:create]

View file

@ -1,6 +1,5 @@
class Api::V1::AgentBotsController < Api::BaseController class Api::V1::AgentBotsController < Api::BaseController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_before_action :check_subscription
def index def index
render json: AgentBot.all render json: AgentBot.all

View file

@ -0,0 +1,7 @@
class Api::V1::Integrations::WebhooksController < ApplicationController
def create
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
response = builder.perform
render json: response
end
end

View file

@ -16,6 +16,6 @@ class Api::V1::ProfilesController < Api::BaseController
end end
def profile_params def profile_params
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar) params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar, :availability)
end end
end end

View file

@ -1,18 +1,6 @@
class Api::V1::WebhooksController < ApplicationController class Api::V1::WebhooksController < ApplicationController
skip_before_action :authenticate_user!, raise: false skip_before_action :authenticate_user!, raise: false
skip_before_action :set_current_user skip_before_action :set_current_user
skip_before_action :check_subscription
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee
chargebee_consumer.consume
head :ok
rescue StandardError => e
Raven.capture_exception(e)
head :ok
end
def twitter_crc def twitter_crc
render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" } render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
@ -34,16 +22,6 @@ class Api::V1::WebhooksController < ApplicationController
end end
end end
def login_from_basic_auth
authenticate_or_request_with_http_basic do |username, password|
username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD']
end
end
def chargebee_consumer
@chargebee_consumer ||= ::Webhooks::Chargebee.new(params)
end
def twitter_consumer def twitter_consumer
@twitter_consumer ||= ::Webhooks::Twitter.new(params) @twitter_consumer ||= ::Webhooks::Twitter.new(params)
end end

View file

@ -1,4 +1,7 @@
class Api::V1::Widget::BaseController < ApplicationController class Api::V1::Widget::BaseController < ApplicationController
before_action :set_web_widget
before_action :set_contact
private private
def conversation def conversation

View file

@ -1,7 +1,4 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def update def update
contact_identify_action = ContactIdentifyAction.new( contact_identify_action = ContactIdentifyAction.new(
contact: @contact, contact: @contact,

View file

@ -1,12 +1,18 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types include Events::Types
before_action :set_web_widget
before_action :set_contact
def index def index
@conversation = conversation @conversation = conversation
end end
def update_last_seen
head :ok && return if conversation.nil?
conversation.user_last_seen_at = DateTime.now.utc
conversation.save!
head :ok
end
def toggle_typing def toggle_typing
head :ok && return if conversation.nil? head :ok && return if conversation.nil?

View file

@ -1,15 +1,20 @@
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
include Events::Types include Events::Types
before_action :set_web_widget
before_action :set_contact
def create def create
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox) Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, event_info: event_info)
head :no_content head :no_content
end end
private private
def event_info
{
widget_language: params[:locale],
browser_language: browser.accept_language.first&.code
}
end
def permitted_params def permitted_params
params.permit(:name, :website_token) params.permit(:name, :website_token)
end end

View file

@ -1,5 +1,5 @@
class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
before_action :set_web_widget skip_before_action :set_contact
def index def index
@inbox_members = @web_widget.inbox.inbox_members.includes(:user) @inbox_members = @web_widget.inbox.inbox_members.includes(:user)

View file

@ -1,7 +1,4 @@
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
def create def create
conversation.label_list.add(permitted_params[:label]) conversation.label_list.add(permitted_params[:label])
conversation.save! conversation.save!

View file

@ -1,6 +1,4 @@
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
before_action :set_conversation, only: [:create] before_action :set_conversation, only: [:create]
before_action :set_message, only: [:update] before_action :set_message, only: [:update]
@ -47,7 +45,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params def message_params
{ {
account_id: conversation.account_id, account_id: conversation.account_id,
contact_id: @contact.id, sender: @contact,
content: permitted_params[:message][:content], content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: :incoming message_type: :incoming

View file

@ -1,6 +1,6 @@
class Api::V2::Accounts::ReportsController < Api::BaseController class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def account def account
builder = V2::ReportBuilder.new(current_account, account_report_params) builder = V2::ReportBuilder.new(Current.account, account_report_params)
data = builder.build data = builder.build
render json: data render json: data
end end
@ -29,7 +29,7 @@ class Api::V2::Accounts::ReportsController < Api::BaseController
end end
def account_summary_metrics def account_summary_metrics
builder = V2::ReportBuilder.new(current_account, account_summary_params) builder = V2::ReportBuilder.new(Current.account, account_summary_params)
builder.summary builder.summary
end end
end end

View file

@ -1,6 +1,5 @@
class ApiController < ApplicationController class ApiController < ApplicationController
skip_before_action :set_current_user, only: [:index] skip_before_action :set_current_user, only: [:index]
skip_before_action :check_subscription, only: [:index]
def index def index
render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) } render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }

View file

@ -5,7 +5,6 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
before_action :set_current_user, unless: :devise_controller? before_action :set_current_user, unless: :devise_controller?
before_action :check_subscription, unless: :devise_controller?
around_action :handle_with_exception, unless: :devise_controller? around_action :handle_with_exception, unless: :devise_controller?
# after_action :verify_authorized # after_action :verify_authorized
@ -13,40 +12,6 @@ class ApplicationController < ActionController::Base
private private
def current_account
@current_account ||= find_current_account
Current.account = @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
switch_locale account
account
end
def switch_locale(account)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= (I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil)
# if local is not set in param, lets try account
locale ||= (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil)
I18n.locale = locale || I18n.default_locale
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
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 def handle_with_exception
yield yield
rescue ActiveRecord::RecordNotFound => e rescue ActiveRecord::RecordNotFound => e
@ -65,7 +30,7 @@ class ApplicationController < ActionController::Base
end end
def current_subscription def current_subscription
@subscription ||= current_account.subscription @subscription ||= Current.account.subscription
end end
def render_unauthorized(message) def render_unauthorized(message)
@ -94,16 +59,20 @@ class ApplicationController < ActionController::Base
render json: exception.to_hash, status: exception.http_status render json: exception.to_hash, status: exception.http_status
end end
def check_subscription def locale_from_params
# This block is left over from the initial version of chatwoot I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil
# We might reuse this later in the hosted version of chatwoot.
return if !ENV['BILLING_ENABLED'] || !current_user
if current_subscription.trial? && current_subscription.expiry < Date.current
render json: { error: 'Trial Expired' }, status: :trial_expired
elsif current_subscription.cancelled?
render json: { error: 'Account Suspended' }, status: :account_suspended
end end
def locale_from_account(account)
I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil
end
def switch_locale(account)
# priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= locale_from_params
# if local is not set in param, lets try account
locale ||= locale_from_account(account)
I18n.locale = locale || I18n.default_locale
end end
def pundit_user def pundit_user

View file

@ -4,6 +4,7 @@ class WidgetsController < ActionController::Base
before_action :set_token before_action :set_token
before_action :set_contact before_action :set_contact
before_action :build_contact before_action :build_contact
after_action :allow_iframe_requests
def index; end def index; end
@ -50,4 +51,8 @@ class WidgetsController < ActionController::Base
def permitted_params def permitted_params
params.permit(:website_token, :cw_conversation) params.permit(:website_token, :cw_conversation)
end end
def allow_iframe_requests
response.headers.delete('X-Frame-Options')
end
end end

View file

@ -9,8 +9,7 @@ class AsyncDispatcher < BaseDispatcher
end end
def listeners def listeners
listeners = [EventListener.instance, WebhookListener.instance] listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners listeners
end end
end end

View file

@ -62,7 +62,7 @@ class ConversationFinder
def find_all_conversations def find_all_conversations
@conversations = current_account.conversations.includes( @conversations = current_account.conversations.includes(
:assignee, :contact, :inbox :assignee, :inbox, contact: [:avatar_attachment]
).where(inbox_id: @inbox_ids) ).where(inbox_id: @inbox_ids)
end end

View file

@ -1,2 +0,0 @@
module Api::V1::SubscriptionsHelper
end

View file

@ -10,6 +10,10 @@ class ApiClient {
} }
get url() { get url() {
return `${this.baseUrl()}/${this.resource}`;
}
baseUrl() {
let url = this.apiVersion; let url = this.apiVersion;
if (this.options.accountScoped) { if (this.options.accountScoped) {
const isInsideAccountScopedURLs = window.location.pathname.includes( const isInsideAccountScopedURLs = window.location.pathname.includes(
@ -21,7 +25,8 @@ class ApiClient {
url = `${url}/accounts/${accountId}`; url = `${url}/accounts/${accountId}`;
} }
} }
return `${url}/${this.resource}`;
return url;
} }
get() { get() {

View file

@ -118,21 +118,18 @@ export default {
return axios.post(urlData.url, { email }); return axios.post(urlData.url, { email });
}, },
profileUpdate({ name, email, password, password_confirmation, avatar }) { profileUpdate({ password, password_confirmation, ...profileAttributes }) {
const formData = new FormData(); const formData = new FormData();
if (name) { Object.keys(profileAttributes).forEach(key => {
formData.append('profile[name]', name); const value = profileAttributes[key];
} if (value) {
if (email) { formData.append(`profile[${key}]`, value);
formData.append('profile[email]', email);
} }
});
if (password && password_confirmation) { if (password && password_confirmation) {
formData.append('profile[password]', password); formData.append('profile[password]', password);
formData.append('profile[password_confirmation]', password_confirmation); formData.append('profile[password_confirmation]', password_confirmation);
} }
if (avatar) {
formData.append('profile[avatar]', avatar);
}
return axios.put(endPoints('profileUpdate').url, formData); return axios.put(endPoints('profileUpdate').url, formData);
}, },
}; };

View file

@ -1,20 +0,0 @@
/* global axios */
import endPoints from './endPoints';
export default {
getSubscription() {
const urlData = endPoints('subscriptions').get();
const fetchPromise = new Promise((resolve, reject) => {
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
reject(error);
});
});
return fetchPromise;
},
};

View file

@ -6,20 +6,6 @@ class FBChannel extends ApiClient {
super('facebook_indicators', { accountScoped: true }); super('facebook_indicators', { accountScoped: true });
} }
markSeen({ inboxId, contactId }) {
return axios.post(`${this.url}/mark_seen`, {
inbox_id: inboxId,
contact_id: contactId,
});
}
toggleTyping({ status, inboxId, contactId }) {
return axios.post(`${this.url}/typing_${status}`, {
inbox_id: inboxId,
contact_id: contactId,
});
}
create(params) { create(params) {
return axios.post( return axios.post(
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`, `${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,

View file

@ -33,14 +33,6 @@ const endPoints = {
}, },
params: { omniauth_token: '' }, params: { omniauth_token: '' },
}, },
subscriptions: {
get() {
return {
url: '/api/v1/subscriptions',
};
},
},
}; };
export default page => { export default page => {

View file

@ -6,13 +6,14 @@ class ConversationApi extends ApiClient {
super('conversations', { accountScoped: true }); super('conversations', { accountScoped: true });
} }
get({ inboxId, status, assigneeType, page }) { get({ inboxId, status, assigneeType, page, labels }) {
return axios.get(this.url, { return axios.get(this.url, {
params: { params: {
inbox_id: inboxId, inbox_id: inboxId,
status, status,
assignee_type: assigneeType, assignee_type: assigneeType,
page, page,
labels,
}, },
}); });
} }
@ -43,6 +44,17 @@ class ConversationApi extends ApiClient {
mute(conversationId) { mute(conversationId) {
return axios.post(`${this.url}/${conversationId}/mute`); return axios.post(`${this.url}/${conversationId}/mute`);
} }
meta({ inboxId, status, assigneeType, labels }) {
return axios.get(`${this.url}/meta`, {
params: {
inbox_id: inboxId,
status,
assignee_type: assigneeType,
labels,
},
});
}
} }
export default new ConversationApi(); export default new ConversationApi();

View file

@ -0,0 +1,21 @@
/* global axios */
import ApiClient from './ApiClient';
class IntegrationsAPI extends ApiClient {
constructor() {
super('integrations/apps', { accountScoped: true });
}
connectSlack(code) {
return axios.post(`${this.baseUrl()}/integrations/slack`, {
code: code,
});
}
delete(integrationId) {
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
}
}
export default new IntegrationsAPI();

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class LabelsAPI extends ApiClient {
constructor() {
super('labels', { accountScoped: true });
}
}
export default new LabelsAPI();

View file

@ -9,7 +9,5 @@ describe('#FBChannel', () => {
expect(fbChannel).toHaveProperty('create'); expect(fbChannel).toHaveProperty('create');
expect(fbChannel).toHaveProperty('update'); expect(fbChannel).toHaveProperty('update');
expect(fbChannel).toHaveProperty('delete'); expect(fbChannel).toHaveProperty('delete');
expect(fbChannel).toHaveProperty('markSeen');
expect(fbChannel).toHaveProperty('toggleTyping');
}); });
}); });

View file

@ -1,14 +1,14 @@
import agents from '../contacts'; import contacts from '../contacts';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
describe('#ContactsAPI', () => { describe('#ContactsAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
expect(agents).toBeInstanceOf(ApiClient); expect(contacts).toBeInstanceOf(ApiClient);
expect(agents).toHaveProperty('get'); expect(contacts).toHaveProperty('get');
expect(agents).toHaveProperty('show'); expect(contacts).toHaveProperty('show');
expect(agents).toHaveProperty('create'); expect(contacts).toHaveProperty('create');
expect(agents).toHaveProperty('update'); expect(contacts).toHaveProperty('update');
expect(agents).toHaveProperty('delete'); expect(contacts).toHaveProperty('delete');
expect(agents).toHaveProperty('getConversations'); expect(contacts).toHaveProperty('getConversations');
}); });
}); });

View file

@ -0,0 +1,19 @@
import conversationAPI from '../../inbox/conversation';
import ApiClient from '../../ApiClient';
describe('#ConversationAPI', () => {
it('creates correct instance', () => {
expect(conversationAPI).toBeInstanceOf(ApiClient);
expect(conversationAPI).toHaveProperty('get');
expect(conversationAPI).toHaveProperty('show');
expect(conversationAPI).toHaveProperty('create');
expect(conversationAPI).toHaveProperty('update');
expect(conversationAPI).toHaveProperty('delete');
expect(conversationAPI).toHaveProperty('toggleStatus');
expect(conversationAPI).toHaveProperty('assignAgent');
expect(conversationAPI).toHaveProperty('markMessageRead');
expect(conversationAPI).toHaveProperty('toggleTyping');
expect(conversationAPI).toHaveProperty('mute');
expect(conversationAPI).toHaveProperty('meta');
});
});

View file

@ -1,7 +1,7 @@
import inboxes from '../inboxes'; import inboxes from '../inboxes';
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
describe('#AgentAPI', () => { describe('#InboxesAPI', () => {
it('creates correct instance', () => { it('creates correct instance', () => {
expect(inboxes).toBeInstanceOf(ApiClient); expect(inboxes).toBeInstanceOf(ApiClient);
expect(inboxes).toHaveProperty('get'); expect(inboxes).toHaveProperty('get');

View file

@ -0,0 +1,14 @@
import labels from '../labels';
import ApiClient from '../ApiClient';
describe('#LabelsAPI', () => {
it('creates correct instance', () => {
expect(labels).toBeInstanceOf(ApiClient);
expect(labels).toHaveProperty('get');
expect(labels).toHaveProperty('show');
expect(labels).toHaveProperty('create');
expect(labels).toHaveProperty('update');
expect(labels).toHaveProperty('delete');
expect(labels.url).toBe('/api/v1/labels');
});
});

View file

@ -1,6 +1,6 @@
.button { .button {
font-weight: $font-weight-medium;
font-family: $body-font-family; font-family: $body-font-family;
font-weight: $font-weight-medium;
&.round { &.round {
border-radius: 1000px; border-radius: 1000px;
@ -20,10 +20,11 @@
} }
.tooltip { .tooltip {
max-width: 15rem;
padding: $space-smaller $space-small;
border-radius: $space-smaller; border-radius: $space-smaller;
font-size: $font-size-mini; font-size: $font-size-mini;
max-width: 15rem;
padding: $space-smaller $space-small;
z-index: 9999;
} }
code { code {

View file

@ -67,6 +67,7 @@ BlinkMacSystemFont,
"Segoe UI", "Segoe UI",
Roboto, Roboto,
"Helvetica Neue", "Helvetica Neue",
Tahoma,
Arial, Arial,
sans-serif; sans-serif;
$body-antialiased: true; $body-antialiased: true;
@ -382,7 +383,7 @@ $label-color: $primary-color;
$label-color-alt: $black; $label-color-alt: $black;
$label-palette: $foundation-palette; $label-palette: $foundation-palette;
$label-font-size: $font-size-micro; $label-font-size: $font-size-micro;
$label-padding: $space-micro $space-smaller; $label-padding: $space-smaller $space-small;
$label-radius: $space-micro; $label-radius: $space-micro;
// 21. Media Object // 21. Media Object

View file

@ -25,7 +25,7 @@
width: $space-medium; width: $space-medium;
&.message { &.message {
@include elegent-shadow; @include normal-shadow;
background: $color-white; background: $color-white;
border-radius: $space-large; border-radius: $space-large;
left: 0; left: 0;

View file

@ -1,6 +1,5 @@
@import '~widget/assets/scss/mixins'; @import '~widget/assets/scss/mixins';
$elegant-shadow-color: rgba(49, 49, 93, 0.15);
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
//borders //borders
@ -141,12 +140,8 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
overflow-y: auto; overflow-y: auto;
} }
@mixin elegent-shadow() {
box-shadow: 0 10px 25px 0 $elegant-shadow-color;
}
@mixin elegant-card() { @mixin elegant-card() {
@include elegent-shadow; @include normal-shadow;
border-radius: $space-small; border-radius: $space-small;
} }
@ -194,28 +189,42 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
border-bottom: $size solid $color; border-bottom: $size solid $color;
border-left: $size solid transparent; border-left: $size solid transparent;
border-right: $size solid transparent; border-right: $size solid transparent;
} @else if $direction == 'right' { }
@else if $direction == 'right' {
border-bottom: $size solid transparent; border-bottom: $size solid transparent;
border-left: $size solid $color; border-left: $size solid $color;
border-top: $size solid transparent; border-top: $size solid transparent;
} @else if $direction == 'bottom' { }
@else if $direction == 'bottom' {
border-left: $size solid transparent; border-left: $size solid transparent;
border-right: $size solid transparent; border-right: $size solid transparent;
border-top: $size solid $color; border-top: $size solid $color;
} @else if $direction == 'left' { }
@else if $direction == 'left' {
border-bottom: $size solid transparent; border-bottom: $size solid transparent;
border-right: $size solid $color; border-right: $size solid $color;
border-top: $size solid transparent; border-top: $size solid transparent;
} @else if $direction == 'top-left' { }
@else if $direction == 'top-left' {
border-right: $size solid transparent; border-right: $size solid transparent;
border-top: $size solid $color; border-top: $size solid $color;
} @else if $direction == 'top-right' { }
@else if $direction == 'top-right' {
border-left: $size solid transparent; border-left: $size solid transparent;
border-top: $size solid $color; border-top: $size solid $color;
} @else if $direction == 'bottom-left' { }
@else if $direction == 'bottom-left' {
border-bottom: $size solid $color; border-bottom: $size solid $color;
border-right: $size solid transparent; border-right: $size solid transparent;
} @else if $direction == 'bottom-right' { }
@else if $direction == 'bottom-right' {
border-bottom: $size solid $color; border-bottom: $size solid $color;
border-left: $size solid transparent; border-left: $size solid transparent;
} }

View file

@ -3,7 +3,6 @@
@import 'animations'; @import 'animations';
@import 'foundation-custom'; @import 'foundation-custom';
@import 'widgets/billing';
@import 'widgets/buttons'; @import 'widgets/buttons';
@import 'widgets/conv-header'; @import 'widgets/conv-header';
@import 'widgets/conversation-card'; @import 'widgets/conversation-card';

View file

@ -3,16 +3,17 @@
background: $color-white; background: $color-white;
border: 1px solid $color-border; border: 1px solid $color-border;
border-radius: $space-smaller; border-radius: $space-smaller;
margin-bottom: $space-normal;
padding: $space-normal; padding: $space-normal;
.integration--image { .integration--image {
display: flex; display: flex;
margin-right: $space-normal; margin-right: $space-normal;
width: 8rem; width: 10rem;
img { img {
max-width: 8rem; max-width: 100%;
padding: $space-small; padding: $space-medium;
} }
} }

View file

@ -1,75 +0,0 @@
.billing {
@include full-height;
.row {
@include full-height;
}
.billing__stats {
@include flex;
}
.billing__form {
@include thin-border($color-border-light);
@include margin($zero - $space-micro);
@include full-height;
background: $color-white;
iframe {
@include full-height;
border: 0;
width: 100%;
}
}
.account-row {
@include padding($space-normal);
@include flex;
flex-direction: column;
// @include thin-border($color-border-light);
// @include margin(-$space-micro $zero);
background: $color-white;
font-size: $font-size-small;
.title {
color: $color-heading;
font-weight: $font-weight-medium;
}
.value {
font-size: $font-size-mega;
font-weight: $font-weight-light;
text-transform: capitalize;
}
}
}
.account-locked {
@include background-gray;
@include margin(0);
}
.lock-message {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
div {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
img {
@include margin($space-normal);
width: 10rem;
}
span {
font-size: $font-size-small;
font-weight: $font-weight-medium;
text-align: center;
}
}
}

View file

@ -66,6 +66,8 @@
} }
.button.resolve--button { .button.resolve--button {
@include flex-align($x: center, $y: middle);
width: 13.2rem; width: 13.2rem;
>.icon { >.icon {

View file

@ -78,6 +78,11 @@
font-size: $font-size-mini; font-size: $font-size-mini;
vertical-align: top; vertical-align: top;
} }
.message-from-agent {
color: $color-gray;
font-size: $font-size-mini;
}
} }
.conversation--meta { .conversation--meta {
@ -120,11 +125,11 @@
} }
.conversation--message { .conversation--message {
font-weight: $font-weight-medium; font-weight: $font-weight-bold;
} }
.conversation--user { .conversation--user {
font-weight: $font-weight-medium; font-weight: $font-weight-bold;
} }
} }

View file

@ -26,6 +26,7 @@
.link { .link {
color: $color-white; color: $color-white;
text-decoration: underline;
} }
} }
@ -308,7 +309,7 @@
&.is-private { &.is-private {
background: lighten($warning-color, 32%); background: lighten($warning-color, 32%);
border: 1px solid $color-border; border: 1px solid lighten($warning-color, 15%);
color: $color-heading; color: $color-heading;
padding-right: $space-large; padding-right: $space-large;
position: relative; position: relative;

View file

@ -67,6 +67,10 @@
font-size: $font-size-small; font-size: $font-size-small;
} }
.content {
@include padding($space-large);
}
form { form {
@include padding($space-large); @include padding($space-large);
align-self: center; align-self: center;

View file

@ -1,10 +1,15 @@
.reply-box { .reply-box {
@include elegant-card; @include light-shadow;
border-bottom: 0; border-bottom: 0;
border-radius: $space-small;
margin: $space-normal; margin: $space-normal;
margin-top: 0; margin-top: 0;
max-height: $space-jumbo * 2; max-height: $space-mega * 3;
transition: height 2s $ease-in-out-cubic; transition: box-shadow .35s $ease-in-out-cubic, height 2s $ease-in-out-cubic;
&.is-focused {
@include normal-shadow;
}
.reply-box__top { .reply-box__top {
@include flex; @include flex;
@ -74,7 +79,8 @@
background: transparent; background: transparent;
// Override min-height : 50px in foundation // Override min-height : 50px in foundation
// //
min-height: 1rem; max-height: $space-mega * 2.4;
min-height: 4rem;
resize: none; resize: none;
} }
} }

View file

@ -3,7 +3,7 @@
<div class="chat-list__top"> <div class="chat-list__top">
<h1 class="page-title"> <h1 class="page-title">
<woot-sidemenu-icon /> <woot-sidemenu-icon />
{{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }} {{ pageTitle }}
</h1> </h1>
<chat-filter @statusFilterChange="updateStatusType" /> <chat-filter @statusFilterChange="updateStatusType" />
</div> </div>
@ -15,14 +15,15 @@
@chatTabChange="updateAssigneeTab" @chatTabChange="updateAssigneeTab"
/> />
<p v-if="!chatListLoading && !getChatsForTab().length" class="content-box"> <p v-if="!chatListLoading && !conversationList.length" class="content-box">
{{ $t('CHAT_LIST.LIST.404') }} {{ $t('CHAT_LIST.LIST.404') }}
</p> </p>
<div class="conversations-list"> <div class="conversations-list">
<conversation-card <conversation-card
v-for="chat in getChatsForTab()" v-for="chat in conversationList"
:key="chat.id" :key="chat.id"
:active-label="label"
:chat="chat" :chat="chat"
/> />
@ -40,7 +41,7 @@
<p <p
v-if=" v-if="
getChatsForTab().length && conversationList.length &&
hasCurrentPageEndReached && hasCurrentPageEndReached &&
!chatListLoading !chatListLoading
" "
@ -55,6 +56,7 @@
<script> <script>
/* eslint-env browser */ /* eslint-env browser */
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ChatFilter from './widgets/conversation/ChatFilter'; import ChatFilter from './widgets/conversation/ChatFilter';
@ -71,7 +73,16 @@ export default {
ChatFilter, ChatFilter,
}, },
mixins: [timeMixin, conversationMixin], mixins: [timeMixin, conversationMixin],
props: ['conversationInbox'], props: {
conversationInbox: {
type: [String, Number],
default: 0,
},
label: {
type: String,
default: '',
},
},
data() { data() {
return { return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@ -87,14 +98,17 @@ export default {
chatListLoading: 'getChatListLoadingStatus', chatListLoading: 'getChatListLoadingStatus',
currentUserID: 'getCurrentUserID', currentUserID: 'getCurrentUserID',
activeInbox: 'getSelectedInbox', activeInbox: 'getSelectedInbox',
convStats: 'getConvTabStats', conversationStats: 'conversationStats/getStats',
}), }),
assigneeTabItems() { assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({ return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
const count = this.conversationStats[item.COUNT_KEY] || 0;
return {
key: item.KEY, key: item.KEY,
name: item.NAME, name: item.NAME,
count: this.convStats[item.COUNT_KEY] || 0, count,
})); };
});
}, },
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox); return this.$store.getters['inboxes/getInbox'](this.activeInbox);
@ -109,16 +123,61 @@ export default {
this.activeAssigneeTab this.activeAssigneeTab
); );
}, },
conversationFilters() {
return {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined,
};
},
pageTitle() {
if (this.inbox.name) {
return this.inbox.name;
}
if (this.label) {
return `#${this.label}`;
}
return this.$t('CHAT_LIST.TAB_HEADING');
},
conversationList() {
let conversationList = [];
if (this.activeAssigneeTab === 'me') {
conversationList = this.mineChatsList.slice();
} else if (this.activeAssigneeTab === 'unassigned') {
conversationList = this.unAssignedChatsList.slice();
} else {
conversationList = this.allChatList.slice();
}
if (!this.label) {
return conversationList;
}
return conversationList.filter(conversation => {
const labels = this.$store.getters[
'conversationLabels/getConversationLabels'
](conversation.id);
return labels.includes(this.label);
});
},
}, },
watch: { watch: {
conversationInbox() { conversationInbox() {
this.resetAndFetchData(); this.resetAndFetchData();
}, },
label() {
this.resetAndFetchData();
},
}, },
mounted() { mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus); this.$store.dispatch('setChatFilter', this.activeStatus);
this.resetAndFetchData(); this.resetAndFetchData();
this.$store.dispatch('agents/get');
bus.$on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters);
});
}, },
methods: { methods: {
resetAndFetchData() { resetAndFetchData() {
@ -127,12 +186,7 @@ export default {
this.fetchConversations(); this.fetchConversations();
}, },
fetchConversations() { fetchConversations() {
this.$store.dispatch('fetchAllConversations', { this.$store.dispatch('fetchAllConversations', this.conversationFilters);
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus,
page: this.currentPage + 1,
});
}, },
updateAssigneeTab(selectedTab) { updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) { if (this.activeAssigneeTab !== selectedTab) {
@ -148,17 +202,6 @@ export default {
this.resetAndFetchData(); this.resetAndFetchData();
} }
}, },
getChatsForTab() {
let copyList = [];
if (this.activeAssigneeTab === 'me') {
copyList = this.mineChatsList.slice();
} else if (this.activeAssigneeTab === 'unassigned') {
copyList = this.unAssignedChatsList.slice();
} else {
copyList = this.allChatList.slice();
}
return copyList;
},
}, },
}; };
</script> </script>

View file

@ -6,6 +6,7 @@ import Code from './Code';
import ColorPicker from './widgets/ColorPicker'; import ColorPicker from './widgets/ColorPicker';
import DeleteModal from './widgets/modal/DeleteModal.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue';
import Input from './widgets/forms/Input.vue'; import Input from './widgets/forms/Input.vue';
import Label from './widgets/Label.vue';
import LoadingState from './widgets/LoadingState'; import LoadingState from './widgets/LoadingState';
import Modal from './Modal'; import Modal from './Modal';
import ModalHeader from './ModalHeader'; import ModalHeader from './ModalHeader';
@ -25,6 +26,7 @@ const WootUIKit = {
DeleteModal, DeleteModal,
Input, Input,
LoadingState, LoadingState,
Label,
Modal, Modal,
ModalHeader, ModalHeader,
ReportStatsCard, ReportStatsCard,

View file

@ -18,21 +18,14 @@
:key="inboxSection.toState" :key="inboxSection.toState"
:menu-item="inboxSection" :menu-item="inboxSection"
/> />
<sidebar-item
v-if="shouldShowInboxes"
:key="labelSection.toState"
:menu-item="labelSection"
/>
</transition-group> </transition-group>
</div> </div>
<!-- this block is only required in the hosted version with billing enabled -->
<transition name="fade" mode="out-in">
<woot-status-bar
v-if="shouldShowStatusBox"
:message="trialMessage"
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
:button-route="{ name: 'billing' }"
:type="statusBarClass"
:show-button="isAdmin"
/>
</transition>
<div class="bottom-nav"> <div class="bottom-nav">
<transition name="menu-slide"> <transition name="menu-slide">
<div <div
@ -63,7 +56,11 @@
</div> </div>
</transition> </transition>
<div class="current-user" @click.prevent="showOptions()"> <div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" /> <thumbnail
:src="currentUser.avatar_url"
:username="currentUser.name"
:status="currentUser.availability_status"
/>
<div class="current-user--data"> <div class="current-user--data">
<h3 class="current-user--name"> <h3 class="current-user--name">
{{ currentUser.name }} {{ currentUser.name }}
@ -108,7 +105,6 @@ import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
import Auth from '../../api/auth'; import Auth from '../../api/auth';
import SidebarItem from './SidebarItem'; import SidebarItem from './SidebarItem';
import WootStatusBar from '../widgets/StatusBar';
import { frontendURL } from '../../helper/URLHelper'; import { frontendURL } from '../../helper/URLHelper';
import Thumbnail from '../widgets/Thumbnail'; import Thumbnail from '../widgets/Thumbnail';
import { getSidebarItems } from '../../i18n/default-sidebar'; import { getSidebarItems } from '../../i18n/default-sidebar';
@ -116,7 +112,6 @@ import { getSidebarItems } from '../../i18n/default-sidebar';
export default { export default {
components: { components: {
SidebarItem, SidebarItem,
WootStatusBar,
Thumbnail, Thumbnail,
}, },
mixins: [clickaway, adminMixin], mixins: [clickaway, adminMixin],
@ -135,12 +130,11 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
daysLeft: 'getTrialLeft',
globalConfig: 'globalConfig/get', globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes', inboxes: 'inboxes/getInboxes',
subscriptionData: 'getSubscription',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole', currentRole: 'getCurrentRole',
accountLabels: 'labels/getLabelsOnSidebar',
}), }),
sidemenuItems() { sidemenuItems() {
return getSidebarItems(this.accountId); return getSidebarItems(this.accountId);
@ -160,10 +154,6 @@ export default {
} }
} }
if (!window.chatwootConfig.billingEnabled) {
menuItems = this.filterBillingRoutes(menuItems);
}
return this.filterMenuItemsByRole(menuItems); return this.filterMenuItemsByRole(menuItems);
}, },
currentRoute() { currentRoute() {
@ -190,38 +180,33 @@ export default {
})), })),
}; };
}, },
labelSection() {
return {
icon: 'ion-pound',
label: 'LABELS',
hasSubMenu: true,
key: 'label',
cssClass: 'menu-title align-justify',
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
toStateName: 'labels_list',
children: this.accountLabels.map(label => ({
id: label.id,
label: label.title,
color: label.color,
toState: frontendURL(
`accounts/${this.accountId}/label/${label.title}`
),
})),
};
},
dashboardPath() { dashboardPath() {
return frontendURL(`accounts/${this.accountId}/dashboard`); return frontendURL(`accounts/${this.accountId}/dashboard`);
}, },
shouldShowStatusBox() {
return (
window.chatwootConfig.billingEnabled &&
(this.subscriptionData.state === 'trial' ||
this.subscriptionData.state === 'cancelled')
);
},
statusBarClass() {
if (this.subscriptionData.state === 'trial') {
return 'warning';
}
if (this.subscriptionData.state === 'cancelled') {
return 'danger';
}
return '';
},
trialMessage() {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
}, },
mounted() { mounted() {
this.$store.dispatch('inboxes/get'); this.$store.dispatch('inboxes/get');
}, },
methods: { methods: {
filterBillingRoutes(menuItems) {
return menuItems.filter(
menuItem => !menuItem.toState.includes('billing')
);
},
filterMenuItemsByRole(menuItems) { filterMenuItemsByRole(menuItems) {
if (!this.currentRole) { if (!this.currentRole) {
return []; return [];

View file

@ -36,7 +36,13 @@
v-if="computedInboxClass(child)" v-if="computedInboxClass(child)"
class="inbox-icon" class="inbox-icon"
:class="computedInboxClass(child)" :class="computedInboxClass(child)"
></i> />
<span
v-if="child.color"
class="label-color--display"
:style="{ backgroundColor: child.color }"
/>
{{ child.label }} {{ child.label }}
</div> </div>
</a> </a>
@ -126,8 +132,22 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
.sub-menu-title { .sub-menu-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.wrap {
display: flex;
align-items: center;
}
.label-color--display {
border-radius: $space-smaller;
height: $space-normal;
margin-right: $space-small;
width: $space-normal;
}
</style> </style>

View file

@ -1,13 +1,25 @@
<template> <template>
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">Back</span> <span class="back-button ion-ios-arrow-left" @click.capture="goBack">
{{ $t('GENERAL_SETTINGS.BACK') }}
</span>
</template> </template>
<script> <script>
import router from '../../routes/index'; import router from '../../routes/index';
export default { export default {
props: {
backUrl: {
type: [String, Object],
default: '',
},
},
methods: { methods: {
goBack() { goBack() {
if (this.backUrl !== '') {
router.push(this.backUrl);
} else {
router.go(-1); router.go(-1);
}
}, },
}, },
}; };

View file

@ -40,10 +40,23 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
enabledFeatures: {
type: Object,
required: true,
},
}, },
methods: { methods: {
isActive(channel) { isActive(channel) {
return ['facebook', 'website', 'twitter', 'twilio'].includes(channel); if (Object.keys(this.enabledFeatures) === 0) {
return false;
}
if (channel === 'facebook') {
return this.enabledFeatures.channel_facebook;
}
if (channel === 'twitter') {
return this.enabledFeatures.channel_facebook;
}
return ['website', 'twilio'].includes(channel);
}, },
onItemClick() { onItemClick() {
if (this.isActive(this.channel)) { if (this.isActive(this.channel)) {

View file

@ -22,11 +22,6 @@ export default {
default: wootConstants.ASSIGNEE_TYPE.ME, default: wootConstants.ASSIGNEE_TYPE.ME,
}, },
}, },
data() {
return {
tabsIndex: wootConstants.ASSIGNEE_TYPE.ME,
};
},
computed: { computed: {
activeTabIndex() { activeTabIndex() {
return this.items.findIndex(item => item.key === this.activeTab); return this.items.findIndex(item => item.key === this.activeTab);

View file

@ -0,0 +1,91 @@
<template>
<div
:class="labelClass"
:style="{ background: bgColor, color: textColor }"
:title="description"
>
<span v-if="!href">{{ title }}</span>
<a v-else :href="href" :style="{ color: textColor }">{{ title }}</a>
<i v-if="showIcon" class="label--icon" :class="icon" @click="onClick" />
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
href: {
type: String,
default: '',
},
bgColor: {
type: String,
default: '#1f93ff',
},
small: {
type: Boolean,
default: false,
},
showIcon: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: 'ion-close',
},
},
computed: {
textColor() {
const color = this.bgColor.replace('#', '');
const r = parseInt(color.slice(0, 2), 16);
const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
// http://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
},
labelClass() {
return `label ${this.small ? 'small' : ''}`;
},
},
methods: {
onClick() {
this.$emit('click', this.title);
},
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
.label {
display: inline-block;
font-size: $font-size-small;
line-height: 1;
margin: $space-micro;
&.small {
font-size: $font-size-mini;
}
a {
&:hover {
text-decoration: underline;
}
}
}
.label--icon {
cursor: pointer;
font-size: $font-size-micro;
line-height: 1.5;
margin-left: $space-smaller;
}
</style>

View file

@ -1,24 +0,0 @@
<template>
<div class="status-bar" :class="type">
<p class="message">{{message}}</p>
<router-link
:to="buttonRoute"
class="button small warning nice"
v-if="showButton"
>
{{buttonText}}
</router-link>
</div>
</template>
<script>
export default {
props: {
message: String,
buttonRoute: Object,
buttonText: String,
showButton: Boolean,
type: String, // Danger, Info, Success, Warning
},
};
</script>

View file

@ -21,11 +21,6 @@
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/fb-badge.png" src="~dashboard/assets/images/fb-badge.png"
/> />
<div
v-else-if="status === 'online'"
class="source-badge user--online"
:style="statusStyle"
></div>
<img <img
v-if="badge === 'Channel::TwitterProfile'" v-if="badge === 'Channel::TwitterProfile'"
id="badge" id="badge"
@ -33,7 +28,6 @@
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png" src="~dashboard/assets/images/twitter-badge.png"
/> />
<img <img
v-if="badge === 'Channel::TwilioSms'" v-if="badge === 'Channel::TwilioSms'"
id="badge" id="badge"
@ -41,6 +35,11 @@
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/channels/whatsapp.png" src="~dashboard/assets/images/channels/whatsapp.png"
/> />
<div
v-if="showStatusIndicator"
:class="`source-badge user-online-status user-online-status--${status}`"
:style="statusStyle"
/>
</div> </div>
</template> </template>
<script> <script>
@ -89,6 +88,9 @@ export default {
}; };
}, },
computed: { computed: {
showStatusIndicator() {
return this.status === 'online' || this.status === 'busy';
},
avatarSize() { avatarSize() {
return Number(this.size.replace(/\D+/g, '')); return Number(this.size.replace(/\D+/g, ''));
}, },
@ -150,8 +152,7 @@ export default {
width: $space-slab; width: $space-slab;
} }
.user--online { .user-online-status {
background: $success-color;
border-radius: 50%; border-radius: 50%;
bottom: $space-micro; bottom: $space-micro;
@ -159,5 +160,13 @@ export default {
content: ' '; content: ' ';
} }
} }
.user-online-status--online {
background: $success-color;
}
.user-online-status--busy {
background: $warning-color;
}
} }
</style> </style>

View file

@ -7,9 +7,10 @@
<Thumbnail <Thumbnail
v-if="!hideThumbnail" v-if="!hideThumbnail"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:badge="currentContact.channel" :badge="chatMetadata.channel"
class="columns" class="columns"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status"
size="40px" size="40px"
/> />
<div class="conversation--details columns"> <div class="conversation--details columns">
@ -23,15 +24,20 @@
{{ inboxName(chat.inbox_id) }} {{ inboxName(chat.inbox_id) }}
</span> </span>
</h4> </h4>
<p <p v-if="lastMessageInChat" class="conversation--message">
class="conversation--message" <i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
v-html="extractMessageText(lastMessageInChat)" <span v-if="lastMessageInChat.content">
/> {{ lastMessageInChat.content }}
</span>
<span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span>
<span v-else>
<i :class="`small-icon ${this.$t(`${attachmentIconKey}.ICON`)}`"></i>
{{ this.$t(`${attachmentIconKey}.CONTENT`) }}
</span>
</p>
<div class="conversation--meta"> <div class="conversation--meta">
<span class="timestamp"> <span class="timestamp">
{{ {{ dynamicTime(chat.timestamp) }}
lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : ''
}}
</span> </span>
<span class="unread">{{ getUnreadCount }}</span> <span class="unread">{{ getUnreadCount }}</span>
</div> </div>
@ -39,11 +45,10 @@
</div> </div>
</template> </template>
<script> <script>
/* eslint no-console: 0 */
/* eslint no-extra-boolean-cast: 0 */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time'; import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
@ -56,6 +61,10 @@ export default {
mixins: [timeMixin, conversationMixin], mixins: [timeMixin, conversationMixin],
props: { props: {
activeLabel: {
type: String,
default: '',
},
chat: { chat: {
type: Object, type: Object,
default: () => {}, default: () => {},
@ -79,12 +88,22 @@ export default {
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
}), }),
chatMetadata() {
return this.chat.meta;
},
currentContact() { currentContact() {
return this.$store.getters['contacts/getContact']( return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id this.chatMetadata.sender.id
); );
}, },
attachmentIconKey() {
const lastMessage = this.lastMessageInChat;
const [{ file_type: fileType } = {}] = lastMessage.attachments;
return `CHAT_LIST.ATTACHMENTS.${fileType}`;
},
isActiveChat() { isActiveChat() {
return this.currentChat.id === this.chat.id; return this.currentChat.id === this.chat.id;
}, },
@ -104,38 +123,25 @@ export default {
lastMessageInChat() { lastMessageInChat() {
return this.lastMessage(this.chat); return this.lastMessage(this.chat);
}, },
messageByAgent() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.OUTGOING;
},
}, },
methods: { methods: {
cardClick(chat) { cardClick(chat) {
const { activeInbox } = this; const { activeInbox } = this;
const path = conversationUrl(this.accountId, activeInbox, chat.id); const path = conversationUrl({
accountId: this.accountId,
activeInbox,
id: chat.id,
label: this.activeLabel,
});
router.push({ path: frontendURL(path) }); router.push({ path: frontendURL(path) });
}, },
extractMessageText(chatItem) {
if (!chatItem) {
return '';
}
const { content, attachments } = chatItem;
if (content) {
return content;
}
if (!attachments) {
return ' ';
}
const [attachment] = attachments;
const { file_type: fileType } = attachment;
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
return `
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
${this.$t(`${key}.CONTENT`)}
`;
},
getEmojiSVG,
inboxName(inboxId) { inboxName(inboxId) {
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId); const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox.name || ''; return stateInbox.name || '';

View file

@ -4,8 +4,9 @@
<Thumbnail <Thumbnail
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
size="40px" size="40px"
:badge="currentContact.channel" :badge="chatMetadata.channel"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status"
/> />
<div class="user--profile__meta"> <div class="user--profile__meta">
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate"> <h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
@ -28,7 +29,7 @@
:allow-empty="true" :allow-empty="true"
deselect-label="Remove" deselect-label="Remove"
placeholder="Select Agent" placeholder="Select Agent"
selected-label="" selected-label
select-label="Assign" select-label="Assign"
track-by="id" track-by="id"
@select="assignAgent" @select="assignAgent"
@ -80,6 +81,10 @@ export default {
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
}), }),
chatMetadata() {
return this.chat.meta;
},
currentContact() { currentContact() {
return this.$store.getters['contacts/getContact']( return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id this.chat.meta.sender.id

View file

@ -121,23 +121,6 @@ export default {
); );
return chat; return chat;
}, },
// Get current FB Page ID
getPageId() {
let stateInbox;
if (this.inboxId) {
const inboxId = Number(this.inboxId);
[stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
} else {
[stateInbox] = this.inboxesList;
}
return !stateInbox ? 0 : stateInbox.page_id;
},
// Get current FB Page ID link
linkToMessage() {
return `https://m.me/${this.getPageId}`;
},
getReadMessages() { getReadMessages() {
const chat = this.getMessages; const chat = this.getMessages;
return chat === undefined ? null : this.readMessages(chat); return chat === undefined ? null : this.readMessages(chat);

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="reply-box"> <div class="reply-box" :class="replyBoxClass">
<div class="reply-box__top" :class="{ 'is-private': isPrivate }"> <div class="reply-box__top" :class="{ 'is-private': isPrivate }">
<canned-response <canned-response
v-if="showCannedResponsesList" v-if="showCannedResponsesList"
@ -13,13 +13,12 @@
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> />
<textarea <resizable-text-area
ref="messageInput" ref="messageInput"
v-model="message" v-model="message"
rows="1"
class="input" class="input"
type="text"
:placeholder="$t(messagePlaceHolder())" :placeholder="$t(messagePlaceHolder())"
:min-height="4"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
/> />
@ -93,18 +92,21 @@ import FileUpload from 'vue-upload-component';
import EmojiInput from '../emoji/EmojiInput'; import EmojiInput from '../emoji/EmojiInput';
import CannedResponse from './CannedResponse'; import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea';
export default { export default {
components: { components: {
EmojiInput, EmojiInput,
CannedResponse, CannedResponse,
FileUpload, FileUpload,
ResizableTextArea,
}, },
mixins: [clickaway], mixins: [clickaway],
data() { data() {
return { return {
message: '', message: '',
isPrivate: false, isPrivate: false,
isFocused: false,
showEmojiPicker: false, showEmojiPicker: false,
showCannedResponsesList: false, showCannedResponsesList: false,
isUploading: { isUploading: {
@ -119,12 +121,7 @@ export default {
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
}), }),
channelType() { channelType() {
const { return this.currentChat.meta.channel;
meta: {
sender: { channel },
},
} = this.currentChat;
return channel;
}, },
conversationType() { conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat; const { additional_attributes: additionalAttributes } = this.currentChat;
@ -143,10 +140,7 @@ export default {
return 10000; return 10000;
}, },
showFileUpload() { showFileUpload() {
return ( return this.channelType === 'Channel::WebWidget';
this.channelType === 'Channel::WebWidget' ||
this.channelType === 'Channel::TwilioSms'
);
}, },
replyButtonLabel() { replyButtonLabel() {
if (this.isPrivate) { if (this.isPrivate) {
@ -157,6 +151,11 @@ export default {
} }
return this.$t('CONVERSATION.REPLYBOX.SEND'); return this.$t('CONVERSATION.REPLYBOX.SEND');
}, },
replyBoxClass() {
return {
'is-focused': this.isFocused,
};
},
}, },
watch: { watch: {
message(val) { message(val) {
@ -211,18 +210,19 @@ export default {
if (this.message.length > this.maxLength) { if (this.message.length > this.maxLength) {
return; return;
} }
const newMessage = this.message;
if (!this.showCannedResponsesList) { if (!this.showCannedResponsesList) {
this.clearMessage();
try { try {
await this.$store.dispatch('sendMessage', { await this.$store.dispatch('sendMessage', {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
message: this.message, message: newMessage,
private: this.isPrivate, private: this.isPrivate,
}); });
this.$emit('scrollToMessage'); this.$emit('scrollToMessage');
} catch (error) { } catch (error) {
// Error // Error
} }
this.clearMessage();
this.hideEmojiPicker(); this.hideEmojiPicker();
} }
}, },
@ -260,16 +260,18 @@ export default {
}, },
onBlur() { onBlur() {
this.isFocused = false;
this.toggleTyping('off'); this.toggleTyping('off');
}, },
onFocus() { onFocus() {
this.isFocused = true;
this.toggleTyping('on'); this.toggleTyping('on');
}, },
toggleTyping(status) { toggleTyping(status) {
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) { if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
const conversationId = this.currentChat.id; const conversationId = this.currentChat.id;
this.$store.dispatch('toggleTyping', { this.$store.dispatch('conversationTypingStatus/toggleTyping', {
status, status,
conversationId, conversationId,
}); });

View file

@ -1,15 +1,8 @@
/* eslint no-console: 0 */ /* eslint no-console: 0 */
import constants from '../constants'; import constants from '../constants';
import Auth from '../api/auth'; import Auth from '../api/auth';
import router from '../routes';
const parseErrorCode = error => { const parseErrorCode = error => {
const errorStatus = error.response ? error.response.status : undefined;
// 901, 902 are used to identify billing related issues
if ([901, 902].includes(errorStatus)) {
const name = Auth.isAdmin() ? 'billing' : 'billing_deactivated';
router.push({ name });
}
return Promise.reject(error); return Promise.reject(error);
}; };

View file

@ -5,11 +5,14 @@ export const frontendURL = (path, params) => {
return `/app/${path}${stringifiedParams}`; return `/app/${path}${stringifiedParams}`;
}; };
export const conversationUrl = (accountId, activeInbox, id) => { export const conversationUrl = ({ accountId, activeInbox, id, label }) => {
const path = activeInbox if (activeInbox) {
? `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}` return `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
: `accounts/${accountId}/conversations/${id}`; }
return path; if (label) {
return `accounts/${accountId}/label/${label}/conversations/${id}`;
}
return `accounts/${accountId}/conversations/${id}`;
}; };
export const accountIdFromPathname = pathname => { export const accountIdFromPathname = pathname => {

View file

@ -1,5 +1,6 @@
import AuthAPI from '../api/auth'; import AuthAPI from '../api/auth';
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
/* global bus */
class ActionCableConnector extends BaseActionCableConnector { class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) { constructor(app, pubsubToken) {
@ -17,6 +18,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.typing_on': this.onTypingOn, 'conversation.typing_on': this.onTypingOn,
'conversation.typing_off': this.onTypingOff, 'conversation.typing_off': this.onTypingOff,
'conversation.contact_changed': this.onConversationContactChange, 'conversation.contact_changed': this.onConversationContactChange,
'presence.update': this.onPresenceUpdate,
}; };
} }
@ -28,6 +30,12 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('updateMessage', data); this.app.$store.dispatch('updateMessage', data);
}; };
onPresenceUpdate = data => {
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
this.app.$store.dispatch('agents/updatePresence', data.users);
this.app.$store.dispatch('setCurrentUserAvailabilityStatus', data.users);
};
onConversationContactChange = payload => { onConversationContactChange = payload => {
const { meta = {}, id: conversationId } = payload; const { meta = {}, id: conversationId } = payload;
const { sender } = meta || {}; const { sender } = meta || {};
@ -45,10 +53,12 @@ class ActionCableConnector extends BaseActionCableConnector {
if (id) { if (id) {
this.app.$store.dispatch('updateAssignee', { id, assignee }); this.app.$store.dispatch('updateAssignee', { id, assignee });
} }
this.fetchConversationStats();
}; };
onConversationCreated = data => { onConversationCreated = data => {
this.app.$store.dispatch('addConversation', data); this.app.$store.dispatch('addConversation', data);
this.fetchConversationStats();
}; };
onLogout = () => AuthAPI.logout(); onLogout = () => AuthAPI.logout();
@ -61,6 +71,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onStatusChange = data => { onStatusChange = data => {
this.app.$store.dispatch('updateConversation', data); this.app.$store.dispatch('updateConversation', data);
this.fetchConversationStats();
}; };
onTypingOn = ({ conversation, user }) => { onTypingOn = ({ conversation, user }) => {
@ -100,6 +111,10 @@ class ActionCableConnector extends BaseActionCableConnector {
this.onTypingOff({ conversation, user }); this.onTypingOff({ conversation, user });
}, 30000); }, 30000);
}; };
fetchConversationStats = () => {
bus.$emit('fetch_conversation_stats');
};
} }
export default { export default {

View file

@ -7,15 +7,20 @@ import {
describe('#URL Helpers', () => { describe('#URL Helpers', () => {
describe('conversationUrl', () => { describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => { it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl(1, undefined, 1)).toBe( expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
'accounts/1/conversations/1' 'accounts/1/conversations/1'
); );
}); });
it('should return ibox conversation URL if activeInbox is not nil', () => { it('should return inbox conversation URL if activeInbox is not nil', () => {
expect(conversationUrl(1, 2, 1)).toBe( expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe(
'accounts/1/inbox/2/conversations/1' 'accounts/1/inbox/2/conversations/1'
); );
}); });
it('should return correct conversation URL if label is active', () => {
expect(
conversationUrl({ accountId: 1, label: 'customer-support', id: 1 })
).toBe('accounts/1/label/customer-support/conversations/1');
});
}); });
describe('frontendURL', () => { describe('frontendURL', () => {
@ -27,16 +32,6 @@ describe('#URL Helpers', () => {
}); });
}); });
/*
export const accountIdFromPathname = pathname => {
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
const accountId = isInsideAccountScopedURLs ? pathname.split('/')[3] : '';
return Number(accountId);
};
*/
describe('accountIdFromPathname', () => { describe('accountIdFromPathname', () => {
it('should return account id if accont scoped url is passed', () => { it('should return account id if accont scoped url is passed', () => {
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1); expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);

View file

@ -8,9 +8,10 @@ export const getSidebarItems = accountId => ({
'inbox_conversation', 'inbox_conversation',
'conversation_through_inbox', 'conversation_through_inbox',
'settings_account_reports', 'settings_account_reports',
'billing_deactivated',
'profile_settings', 'profile_settings',
'profile_settings_index', 'profile_settings_index',
'label_conversations',
'conversations_through_label',
], ],
menuItems: { menuItems: {
assignedToMe: { assignedToMe: {
@ -41,9 +42,8 @@ export const getSidebarItems = accountId => ({
settings: { settings: {
routes: [ routes: [
'agent_list', 'agent_list',
'agent_new',
'canned_list', 'canned_list',
'canned_new', 'labels_list',
'settings_inbox', 'settings_inbox',
'settings_inbox_new', 'settings_inbox_new',
'settings_inbox_list', 'settings_inbox_list',
@ -51,9 +51,9 @@ export const getSidebarItems = accountId => ({
'settings_inboxes_page_channel', 'settings_inboxes_page_channel',
'settings_inboxes_add_agents', 'settings_inboxes_add_agents',
'settings_inbox_finish', 'settings_inbox_finish',
'billing',
'settings_integrations', 'settings_integrations',
'settings_integrations_webhook', 'settings_integrations_webhook',
'settings_integrations_integration',
'general_settings', 'general_settings',
'general_settings_index', 'general_settings_index',
], ],
@ -79,6 +79,13 @@ export const getSidebarItems = accountId => ({
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list', toStateName: 'settings_inbox_list',
}, },
labels: {
icon: 'ion-pricetags',
label: 'LABELS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
},
cannedResponses: { cannedResponses: {
icon: 'ion-chatbox-working', icon: 'ion-chatbox-working',
label: 'CANNED_RESPONSES', label: 'CANNED_RESPONSES',
@ -88,13 +95,6 @@ export const getSidebarItems = accountId => ({
), ),
toStateName: 'canned_list', toStateName: 'canned_list',
}, },
billing: {
icon: 'ion-card',
label: 'BILLING',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing',
},
settings_integrations: { settings_integrations: {
icon: 'ion-flash', icon: 'ion-flash',
label: 'INTEGRATIONS', label: 'INTEGRATIONS',

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