diff --git a/.circleci/config.yml b/.circleci/config.yml index 9579c23a8..18a7a0af9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,7 @@ defaults: &defaults - image: circleci/redis:alpine environment: - CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f - + - RAILS_LOG_TO_STDOUT: false jobs: build: <<: *defaults @@ -69,11 +69,11 @@ jobs: - run: name: Download cc-test-reporter command: | - mkdir -p tmp/ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter - chmod +x ./tmp/cc-test-reporter + mkdir -p ~/tmp + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter + chmod +x ~/tmp/cc-test-reporter - persist_to_workspace: - root: tmp + root: ~/tmp paths: - cc-test-reporter @@ -98,10 +98,10 @@ jobs: - run: name: Run backend tests command: | - bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json + 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 - persist_to_workspace: - root: tmp + root: ~/tmp paths: - codeclimate.backend.json @@ -109,21 +109,23 @@ jobs: name: Run frontend tests command: | 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: - root: tmp + root: ~/tmp paths: - codeclimate.frontend.json # collect reports - store_test_results: - path: /tmp/test-results + path: ~/tmp/test-results - store_artifacts: - path: /tmp/test-results + path: ~/tmp/test-results destination: test-results + - store_artifacts: + path: log - run: name: Upload coverage results to Code Climate command: | - ./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 sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json + ~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json diff --git a/.env.example b/.env.example index d4fb1ea12..5adb4a848 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,7 @@ MANDRILL_INGRESS_API_KEY= ACTIVE_STORAGE_SERVICE=local # Amazon S3 +# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage S3_BUCKET_NAME= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= @@ -74,34 +75,35 @@ LOG_LEVEL=info LOG_SIZE=500 ### 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_APP_SECRET= FB_APP_ID= # Twitter +# documentation: https://www.chatwoot.com/docs/twitter-app-setup TWITTER_APP_ID= TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_SECRET= TWITTER_ENVIRONMENT= +#slack integration +SLACK_CLIENT_ID= +SLACK_CLIENT_SECRET= + ### Change this env variable only if you are using a custom build mobile app ## Mobile app env variables 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 ## generate a new key value here : https://d3v.one/vapid-key-generator/ # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= +# +# for mobile apps +# FCM_SERVER_KEY= ## Bot Customizations USE_INBOX_AVATAR_FOR_BOT=true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0c0eeccc5..48a02cff6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ open_collective: chatwoot +github: chatwoot diff --git a/.rubocop.yml b/.rubocop.yml index fac44c67a..ab5efd6c8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,8 +8,17 @@ Lint/RaiseException: Enabled: true Lint/StructNewOverride: Enabled: true +Lint/DeprecatedOpenSSLConstant: + Enabled: true +Lint/MixedRegexpCaptureTypes: + Enabled: true Layout/LineLength: Max: 150 +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true +Layout/SpaceAroundMethodCallOperator: + Enabled: true + Metrics/ClassLength: Max: 125 Exclude: @@ -18,6 +27,8 @@ RSpec/ExampleLength: Max: 25 Style/Documentation: Enabled: false +Style/ExponentialNotation: + Enabled: false Style/FrozenStringLiteralComment: Enabled: false Style/SymbolArray: @@ -28,6 +39,14 @@ Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true +Style/RedundantFetchBlock: + Enabled: true +Style/RedundantRegexpCharacterClass: + Enabled: true +Style/RedundantRegexpEscape: + Enabled: true +Style/SlicingWithRange: + Enabled: true Style/GlobalVars: Exclude: - 'config/initializers/redis.rb' @@ -65,7 +84,6 @@ Style/GuardClause: - 'app/builders/account_builder.rb' - 'app/models/attachment.rb' - 'app/models/message.rb' - - 'lib/webhooks/chargebee.rb' - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' Metrics/AbcSize: Exclude: @@ -103,8 +121,8 @@ AllCops: Exclude: - 'bin/**/*' - 'db/schema.rb' - - 'config/**/*' - 'public/**/*' + - 'config/initializers/bot.rb' - 'vendor/**/*' - 'node_modules/**/*' - 'lib/tasks/auto_annotate_models.rake' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 06a287ae1..2d3abda7f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -88,7 +88,6 @@ Naming/MemoizedInstanceVariableName: - 'app/controllers/application_controller.rb' - 'app/models/message.rb' - 'lib/integrations/widget/outgoing_message_builder.rb' - - 'lib/webhooks/chargebee.rb' # Offense count: 4 # Cop supports --auto-correct. @@ -187,7 +186,6 @@ Rails/EnumHash: - 'app/models/attachment.rb' - 'app/models/conversation.rb' - 'app/models/message.rb' - - 'app/models/subscription.rb' - 'app/models/user.rb' # Offense count: 1 @@ -226,7 +224,6 @@ Rails/Output: Rails/TimeZone: Exclude: - 'app/builders/report_builder.rb' - - 'app/models/subscription.rb' - 'lib/reports/update_account_identity.rb' - 'lib/reports/update_agent_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/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 # Configuration parameters: AllowIfModifier. Style/IfInsideElse: diff --git a/.scss-lint.yml b/.scss-lint.yml index 481d94c70..19fae0cf1 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -51,7 +51,7 @@ linters: ElsePlacement: enabled: true - style: same_line # or 'new_line' + style: new_line EmptyLineBetweenBlocks: enabled: true diff --git a/Gemfile b/Gemfile index 50e3e2ce8..a769187de 100644 --- a/Gemfile +++ b/Gemfile @@ -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/ gem 'wisper', '2.0.0' -##--- gems for billing ---## -gem 'chargebee' - ##--- gems for channels ---## gem 'facebook-messenger' gem 'telegram-bot-ruby' @@ -68,6 +65,8 @@ gem 'twilio-ruby', '~> 5.32.0' gem 'twitty' # facebook client gem 'koala' +# slack client +gem 'slack-ruby-client' # Random name generator gem 'haikunator' @@ -84,6 +83,7 @@ gem 'sidekiq' gem 'flag_shih_tzu' ##-- Push notification service --## +gem 'fcm' gem 'webpush' group :development do @@ -117,4 +117,5 @@ group :development, :test do gem 'simplecov', '0.17.1', require: false gem 'spring' gem 'spring-watcher-listen' + gem 'webmock' end diff --git a/Gemfile.lock b/Gemfile.lock index 48e4e12a8..40e05f084 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,56 +18,56 @@ GEM specs: action-cable-testing (0.6.1) actioncable (>= 5.0) - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) + actioncable (6.0.3.2) + actionpack (= 6.0.3.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + actionmailbox (6.0.3.2) + actionpack (= 6.0.3.2) + activejob (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) + actionmailer (6.0.3.2) + actionpack (= 6.0.3.2) + actionview (= 6.0.3.2) + activejob (= 6.0.3.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) + actionpack (6.0.3.2) + actionview (= 6.0.3.2) + activesupport (= 6.0.3.2) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + actiontext (6.0.3.2) + actionpack (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) + actionview (6.0.3.2) + activesupport (= 6.0.3.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) + activejob (6.0.3.2) + activesupport (= 6.0.3.2) globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) + activemodel (6.0.3.2) + activesupport (= 6.0.3.2) + activerecord (6.0.3.2) + activemodel (= 6.0.3.2) + activesupport (= 6.0.3.2) + activestorage (6.0.3.2) + actionpack (= 6.0.3.2) + activejob (= 6.0.3.2) + activerecord (= 6.0.3.2) marcel (~> 0.3.1) - activesupport (6.0.3.1) + activesupport (6.0.3.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -91,26 +91,26 @@ GEM annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) - ast (2.4.0) - attr_extras (6.2.3) - autoprefixer-rails (9.7.6) + ast (2.4.1) + attr_extras (6.2.4) + autoprefixer-rails (9.8.2) execjs aws-eventstream (1.1.0) - aws-partitions (1.317.0) - aws-sdk-core (3.96.1) + aws-partitions (1.332.0) + aws-sdk-core (3.100.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.31.0) - aws-sdk-core (~> 3, >= 3.71.0) + aws-sdk-kms (1.34.1) + aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.65.0) - aws-sdk-core (~> 3, >= 3.96.1) + aws-sdk-s3 (1.69.1) + aws-sdk-core (~> 3, >= 3.99.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.3) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sigv4 (1.2.0) + aws-eventstream (~> 1, >= 1.0.2) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -127,25 +127,24 @@ GEM bootsnap (1.4.6) msgpack (~> 1.0) brakeman (4.8.2) - browser (4.1.0) + browser (4.2.0) builder (3.2.4) bullet (6.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundle-audit (0.1.0) bundler-audit - bundler-audit (0.6.1) + bundler-audit (0.7.0.1) bundler (>= 1.2.0, < 3) - thor (~> 0.18) + thor (>= 0.18, < 2) byebug (11.1.3) - chargebee (2.7.5) - json_pure (~> 2.1) - rest-client (>= 1.8, < 3.0) - coderay (1.1.2) + coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) 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) datetime_picker_rails (0.0.7) momentjs-rails (>= 2.8.1) @@ -153,17 +152,18 @@ GEM declarative-option (0.1.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (4.7.1) + devise (4.7.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_token_auth (1.1.3) + devise_token_auth (1.1.4) bcrypt (~> 3.0) devise (> 3.5.2, < 5) rails (>= 4.2.0, < 6.1) - diff-lcs (1.3) + sprockets (= 3.7.2) + diff-lcs (1.4) digest-crc (0.5.1) docile (1.3.2) domain_name (0.5.20190701) @@ -178,23 +178,26 @@ GEM facebook-messenger (1.5.0) httparty (~> 0.13, >= 0.13.7) rack (>= 1.4.5) - factory_bot (5.2.0) - activesupport (>= 4.2.0) - factory_bot_rails (5.2.0) - factory_bot (~> 5.2.0) - railties (>= 4.2.0) - faker (2.11.0) + factory_bot (6.0.2) + activesupport (>= 5.0.0) + factory_bot_rails (6.0.0) + factory_bot (~> 6.0.0) + railties (>= 5.0.0) + faker (2.12.0) i18n (>= 1.6, < 2) faraday (1.0.1) multipart-post (>= 1.2, < 3) faraday_middleware (1.0.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) foreman (0.87.1) + gli (2.19.1) globalid (0.4.2) activesupport (>= 4.2.0) - google-api-client (0.39.4) + google-api-client (0.41.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -205,17 +208,17 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 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) - google-cloud-errors (1.0.0) - google-cloud-storage (1.26.1) + google-cloud-errors (1.0.1) + google-cloud-storage (1.26.2) addressable (~> 2.5) digest-crc (~> 0.4) google-api-client (~> 0.33) google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.12.0) + googleauth (0.13.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -226,16 +229,17 @@ GEM activesupport (>= 5) haikunator (1.1.0) hana (1.3.6) + hashdiff (1.0.1) hashie (4.1.0) hkdf (0.3.0) http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) - httparty (0.18.0) + httparty (0.18.1) mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.8.2) + i18n (1.8.3) concurrent-ruby (~> 1.0) ice_nine (0.11.2) inflecto (0.0.2) @@ -247,7 +251,6 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.3.0) - json_pure (2.3.0) jwt (2.2.1) kaminari (1.2.1) activesupport (>= 4.1.0) @@ -272,7 +275,7 @@ GEM listen (3.2.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.5.0) + loofah (2.6.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -302,9 +305,9 @@ GEM oauth (0.5.4) orm_adapter (0.5.0) os (1.1.0) - parallel (1.19.1) - parser (2.7.1.2) - ast (~> 2.4.0) + parallel (1.19.2) + parser (2.7.1.4) + ast (~> 2.4.1) pg (1.2.3) pry (0.13.1) coderay (~> 1.1) @@ -316,8 +319,8 @@ GEM nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) - rack (2.2.2) - rack-cache (1.11.1) + rack (2.2.3) + rack-cache (1.12.0) rack (>= 0.4) rack-cors (1.1.1) rack (>= 2.0.0) @@ -327,29 +330,29 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) + rails (6.0.3.2) + actioncable (= 6.0.3.2) + actionmailbox (= 6.0.3.2) + actionmailer (= 6.0.3.2) + actionpack (= 6.0.3.2) + actiontext (= 6.0.3.2) + actionview (= 6.0.3.2) + activejob (= 6.0.3.2) + activemodel (= 6.0.3.2) + activerecord (= 6.0.3.2) + activestorage (= 6.0.3.2) + activesupport (= 6.0.3.2) bundler (>= 1.3.0) - railties (= 6.0.3.1) + railties (= 6.0.3.2) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) + railties (6.0.3.2) + actionpack (= 6.0.3.2) + activesupport (= 6.0.3.2) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -358,19 +361,20 @@ GEM rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.1.4) + redis (4.2.1) redis-namespace (1.7.0) redis (>= 3.0.4) redis-rack-cache (2.2.1) rack-cache (>= 1.10, < 2) redis-store (>= 1.6, < 2) - redis-store (1.8.2) + redis-store (1.9.0) redis (>= 4, < 5) + regexp_parser (1.7.1) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) uber (< 0.2.0) - responders (3.0.0) + responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) rest-client (2.1.0) @@ -397,28 +401,33 @@ GEM rspec-mocks (~> 3.9) rspec-support (~> 3.9) rspec-support (3.9.3) - rubocop (0.83.0) + rubocop (0.86.0) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.7) rexml + rubocop-ast (>= 0.0.3, < 1.0) ruby-progressbar (~> 1.7) 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-rails (2.5.2) - activesupport + rubocop-rails (2.6.0) + activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.72.0) - rubocop-rspec (1.39.0) + rubocop (>= 0.82.0) + rubocop-rspec (1.40.0) rubocop (>= 0.68.1) ruby-progressbar (1.10.1) + safe_yaml (1.0.5) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sassc (2.3.0) + sassc (2.4.0) ffi (~> 1.9) sassc-rails (2.1.2) railties (>= 4.0.0) @@ -454,11 +463,18 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) 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-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.0.0) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) @@ -470,7 +486,7 @@ GEM inflecto virtus telephone_number (1.4.7) - thor (0.20.3) + thor (1.0.1) thread_safe (0.3.6) tilt (2.0.10) time_diff (0.3.0) @@ -504,11 +520,15 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.8) rack (>= 2.0.6) - web-console (4.0.2) + web-console (4.0.3) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.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) activesupport (>= 5.2) rack-proxy (>= 0.6.1) @@ -517,9 +537,9 @@ GEM webpush (1.0.0) hkdf (~> 0.2) jwt (~> 2.0) - websocket-driver (0.7.1) + websocket-driver (0.7.2) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) + websocket-extensions (0.1.5) wisper (2.0.0) zeitwerk (2.3.0) @@ -540,13 +560,13 @@ DEPENDENCIES bullet bundle-audit byebug - chargebee devise devise_token_auth dotenv-rails facebook-messenger factory_bot_rails faker + fcm flag_shih_tzu foreman google-cloud-storage @@ -585,6 +605,7 @@ DEPENDENCIES shoulda-matchers sidekiq simplecov (= 0.17.1) + slack-ruby-client spring spring-watcher-listen telegram-bot-ruby @@ -596,6 +617,7 @@ DEPENDENCIES uglifier valid_email2 web-console + webmock webpacker webpush wisper (= 2.0.0) diff --git a/app/actions/contact_merge_action.rb b/app/actions/contact_merge_action.rb index 343e78032..f2d9b6d06 100644 --- a/app/actions/contact_merge_action.rb +++ b/app/actions/contact_merge_action.rb @@ -29,7 +29,7 @@ class ContactMergeAction end 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 def merge_contact_inboxes diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 0eeba6b95..a8251edb4 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -117,7 +117,8 @@ class Messages::MessageBuilder inbox_id: conversation.inbox_id, message_type: @message_type, content: response.content, - source_id: response.identifier + source_id: response.identifier, + sender: contact } end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb index 1b15f24c0..78572b0ee 100644 --- a/app/builders/messages/outgoing/normal_builder.rb +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -37,7 +37,7 @@ class Messages::Outgoing::NormalBuilder message_type: :outgoing, content: @content, private: @private, - user_id: @user&.id, + sender: @user, source_id: @fb_id, content_type: @content_type, items: @items diff --git a/app/builders/notification_subscription_builder.rb b/app/builders/notification_subscription_builder.rb index 05b0bf984..c1574bfc1 100644 --- a/app/builders/notification_subscription_builder.rb +++ b/app/builders/notification_subscription_builder.rb @@ -12,6 +12,8 @@ class NotificationSubscriptionBuilder def identifier @identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push' + @identifier ||= params[:subscription_attributes][:device_id] if params[:subscription_type] == 'fcm' + @identifier end def identifier_subscription diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb index df1a05d6b..ab3af0c92 100644 --- a/app/channels/room_channel.rb +++ b/app/channels/room_channel.rb @@ -1,10 +1,47 @@ class RoomChannel < ApplicationCable::Channel def subscribed - stream_from params[:pubsub_token] - ::OnlineStatusTracker.add_subscription(params[:pubsub_token]) + ensure_stream + current_user + current_account + update_subscription + broadcast_presence end - def unsubscribed - ::OnlineStatusTracker.remove_subscription(params[:pubsub_token]) + def update_presence + 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 diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 8db37ced7..1b9fa24cc 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -10,12 +10,4 @@ class Api::BaseController < ApplicationController def authenticate_by_access_token? request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present? end - - def set_conversation - @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) - end - - def check_billing_enabled - raise ActionController::RoutingError, 'Not Found' unless ENV['BILLING_ENABLED'] - end end diff --git a/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb index 1296b6a55..9daee7b04 100644 --- a/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb +++ b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb @@ -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_mergee_contact, only: [:create] def create contact_merge_action = ContactMergeAction.new( - account: current_account, + account: Current.account, base_contact: @base_contact, mergee_contact: @mergee_contact ) @@ -23,6 +23,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::BaseController end def contacts - @contacts ||= current_account.contacts + @contacts ||= Current.account.contacts end end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 85a05fbc1..030c08045 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -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 :check_authorization before_action :find_user, only: [:create] @@ -46,7 +46,7 @@ class Api::V1::Accounts::AgentsController < Api::BaseController def save_account_user AccountUser.create!( - account_id: current_account.id, + account_id: Current.account.id, user_id: @user.id, role: new_agent_params[:role], inviter_id: current_user.id @@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::BaseController end def agents - @agents ||= current_account.users + @agents ||= Current.account.users end end diff --git a/app/controllers/api/v1/accounts/base_controller.rb b/app/controllers/api/v1/accounts/base_controller.rb new file mode 100644 index 000000000..01a0746b7 --- /dev/null +++ b/app/controllers/api/v1/accounts/base_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 54c7bace4..77e46d669 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -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] def register_facebook_page @@ -7,11 +7,11 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController page_id = params[:page_id] inbox_name = params[:inbox_name] 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_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) rescue StandardError => 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')) end - # get params[:inbox_id], current_account, params[:omniauth_token] + # get params[:inbox_id], current_account. params[:omniauth_token] def reauthorize_page if @inbox&.facebook? fb_page_id = @inbox.channel.page_id @@ -40,7 +40,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController private def inbox - @inbox = current_account.inboxes.find_by(id: params[:inbox_id]) + @inbox = Current.account.inboxes.find_by(id: params[:inbox_id]) end def update_fb_page(fb_page_id, access_token) @@ -50,7 +50,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController end 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 def fb_object @@ -69,7 +69,7 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController return [] if data.empty? data.inject([]) do |result, page_detail| - page_detail[:exists] = current_account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false + page_detail[:exists] = Current.account.facebook_pages.exists?(page_id: page_detail['id']) ? true : false result << page_detail end end diff --git a/app/controllers/api/v1/accounts/canned_responses_controller.rb b/app/controllers/api/v1/accounts/canned_responses_controller.rb index b76da5f8c..16e16e4a6 100644 --- a/app/controllers/api/v1/accounts/canned_responses_controller.rb +++ b/app/controllers/api/v1/accounts/canned_responses_controller.rb @@ -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] def index @@ -6,7 +6,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController end 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! render json: @canned_response end @@ -24,7 +24,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController private def fetch_canned_response - @canned_response = current_account.canned_responses.find(params[:id]) + @canned_response = Current.account.canned_responses.find(params[:id]) end def canned_response_params @@ -33,9 +33,9 @@ class Api::V1::Accounts::CannedResponsesController < Api::BaseController def canned_responses if params[:search] - current_account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%") + Current.account.canned_responses.where('short_code ILIKE ?', "#{params[:search]}%") else - current_account.canned_responses + Current.account.canned_responses end end end diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index 8011d3891..ffbc78eb5 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -1,5 +1,4 @@ -class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController - before_action :current_account +class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts::BaseController before_action :authorize_request def create @@ -38,13 +37,13 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseControlle end def build_inbox - @twilio_channel = current_account.twilio_sms.create!( + @twilio_channel = Current.account.twilio_sms.create!( account_sid: permitted_params[:account_sid], auth_token: permitted_params[:auth_token], phone_number: phone_number, medium: medium ) - @inbox = current_account.inboxes.create( + @inbox = Current.account.inboxes.create( name: permitted_params[:name], channel: @twilio_channel ) diff --git a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb index 8fcb4df13..5c7bf77d7 100644 --- a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb @@ -1,6 +1,6 @@ -class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController +class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::BaseController def index - @conversations = current_account.conversations.includes( + @conversations = Current.account.conversations.includes( :assignee, :contact, :inbox ).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id]) end @@ -9,7 +9,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::BaseController def inbox_ids if current_user.administrator? - current_account.inboxes.pluck(:id) + Current.account.inboxes.pluck(:id) elsif current_user.agent? current_user.assigned_inboxes.pluck(:id) else diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index a3713660b..2208a2cf6 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -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 before_action :check_authorization before_action :fetch_contact, only: [:show, :update] def index - @contacts = current_account.contacts + @contacts = Current.account.contacts end def show; end def create - @contact = Contact.new(contact_create_params) + @contact = Current.account.contacts.new(contact_create_params) @contact.save! render json: @contact end @@ -31,10 +31,10 @@ class Api::V1::Accounts::ContactsController < Api::BaseController end def fetch_contact - @contact = current_account.contacts.find(params[:id]) + @contact = Current.account.contacts.find(params[:id]) end 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 diff --git a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb index 6da3c05da..58fbc5b4e 100644 --- a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb @@ -1,10 +1,8 @@ -class Api::V1::Accounts::Conversations::AssignmentsController < Api::BaseController - before_action :set_conversation, only: [:create] - +class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController # assign agent to a conversation def create # if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation - assignee = current_account.users.find_by(id: params[:assignee_id]) + assignee = Current.account.users.find_by(id: params[:assignee_id]) @conversation.update_assignee(assignee) render json: assignee end diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb new file mode 100644 index 000000000..2dae59abd --- /dev/null +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/conversations/labels_controller.rb b/app/controllers/api/v1/accounts/conversations/labels_controller.rb index 3e80e2825..306544200 100644 --- a/app/controllers/api/v1/accounts/conversations/labels_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/labels_controller.rb @@ -1,6 +1,4 @@ -class Api::V1::Accounts::Conversations::LabelsController < Api::BaseController - before_action :set_conversation, only: [:create, :index] - +class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController def create @conversation.update_labels(params[:labels]) @labels = @conversation.label_list diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index d0ed39edb..c4d595771 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -1,12 +1,11 @@ -class Api::V1::Accounts::Conversations::MessagesController < Api::BaseController - before_action :set_conversation, only: [:index, :create] - +class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController def index @messages = message_finder.perform end 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 end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index fcda432a5..8d8099fdc 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -1,6 +1,5 @@ -class Api::V1::Accounts::ConversationsController < Api::BaseController +class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController include Events::Types - before_action :current_account before_action :conversation, except: [:index] before_action :contact_inbox, only: [:create] @@ -62,7 +61,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController end def conversation - @conversation ||= current_account.conversations.find_by(display_id: params[:id]) + @conversation ||= Current.account.conversations.find_by(display_id: params[:id]) end def contact_inbox @@ -71,7 +70,7 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController def conversation_params { - account_id: current_account.id, + account_id: Current.account.id, inbox_id: @contact_inbox.inbox_id, contact_id: @contact_inbox.contact_id, contact_inbox_id: @contact_inbox.id diff --git a/app/controllers/api/v1/accounts/facebook_indicators_controller.rb b/app/controllers/api/v1/accounts/facebook_indicators_controller.rb index 7cea774cf..a0e42c040 100644 --- a/app/controllers/api/v1/accounts/facebook_indicators_controller.rb +++ b/app/controllers/api/v1/accounts/facebook_indicators_controller.rb @@ -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 around_action :handle_with_exception @@ -38,7 +38,7 @@ class Api::V1::Accounts::FacebookIndicatorsController < Api::BaseController end def inbox - @inbox ||= current_account.inboxes.find(permitted_params[:inbox_id]) + @inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id]) end def set_access_token diff --git a/app/controllers/api/v1/accounts/inbox_members_controller.rb b/app/controllers/api/v1/accounts/inbox_members_controller.rb index f71b3869d..9ad2ef93d 100644 --- a/app/controllers/api/v1/accounts/inbox_members_controller.rb +++ b/app/controllers/api/v1/accounts/inbox_members_controller.rb @@ -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 :current_agents_ids, only: [:create] @@ -12,7 +12,7 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController end 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 private @@ -40,6 +40,6 @@ class Api::V1::Accounts::InboxMembersController < Api::BaseController end def fetch_inbox - @inbox = current_account.inboxes.find(params[:inbox_id]) + @inbox = Current.account.inboxes.find(params[:inbox_id]) end end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 8fdde8cc0..45a190bfb 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -1,17 +1,21 @@ -class Api::V1::Accounts::InboxesController < Api::BaseController - before_action :current_account +class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :fetch_inbox, except: [:index, :create] before_action :fetch_agent_bot, only: [:set_agent_bot] before_action :check_authorization def index - @inboxes = policy_scope(current_account.inboxes) + @inboxes = policy_scope(Current.account.inboxes) end def create ActiveRecord::Base.transaction do channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget' - @inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel) + @inbox = 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.save! end @@ -41,7 +45,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController private def fetch_inbox - @inbox = current_account.inboxes.find(params[:id]) + @inbox = Current.account.inboxes.find(params[:id]) end def fetch_agent_bot @@ -49,7 +53,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController end def web_widgets - current_account.web_widgets + Current.account.web_widgets end def check_authorization @@ -57,11 +61,12 @@ class Api::V1::Accounts::InboxesController < Api::BaseController end 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 def inbox_update_params - params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title, - :welcome_tagline, :agent_away_message]) + params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled, + channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline]) end end diff --git a/app/controllers/api/v1/accounts/integrations/apps_controller.rb b/app/controllers/api/v1/accounts/integrations/apps_controller.rb new file mode 100644 index 000000000..f88cc7c5f --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/apps_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/integrations/slack_controller.rb b/app/controllers/api/v1/accounts/integrations/slack_controller.rb new file mode 100644 index 000000000..77cf05ade --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/slack_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/labels_controller.rb b/app/controllers/api/v1/accounts/labels_controller.rb index c9f15bdae..12c026e66 100644 --- a/app/controllers/api/v1/accounts/labels_controller.rb +++ b/app/controllers/api/v1/accounts/labels_controller.rb @@ -1,10 +1,38 @@ -class Api::V1::Accounts::LabelsController < Api::BaseController - # list all labels in account +class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController + before_action :current_account + before_action :fetch_label, except: [:index, :create] + before_action :check_authorization + def index - @labels = current_account.all_conversation_tags + @labels = policy_scope(Current.account.labels) end - def most_used - @labels = ActsAsTaggableOn::Tag.most_used(params[:count] || 10) + def show; end + + 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 diff --git a/app/controllers/api/v1/accounts/notification_settings_controller.rb b/app/controllers/api/v1/accounts/notification_settings_controller.rb index 14496969d..febad1a98 100644 --- a/app/controllers/api/v1/accounts/notification_settings_controller.rb +++ b/app/controllers/api/v1/accounts/notification_settings_controller.rb @@ -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 def show; end @@ -16,7 +16,7 @@ class Api::V1::Accounts::NotificationSettingsController < Api::BaseController end 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 def notification_setting_params diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index 5d9a5ea54..8ae1629ca 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -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 before_action :fetch_notification, only: [:update] + before_action :set_primary_actor, only: [:read_all] def index - @notifications = current_user.notifications.where(account_id: current_account.id) - render json: @notifications + @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count + @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 def update @@ -15,6 +27,13 @@ class Api::V1::Accounts::NotificationsController < Api::BaseController 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 @notification = current_user.notifications.find(params[:id]) end diff --git a/app/controllers/api/v1/accounts/subscriptions_controller.rb b/app/controllers/api/v1/accounts/subscriptions_controller.rb deleted file mode 100644 index f9b3141d6..000000000 --- a/app/controllers/api/v1/accounts/subscriptions_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index d3afba2af..9e61904d6 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -1,14 +1,13 @@ -class Api::V1::Accounts::WebhooksController < Api::BaseController - before_action :current_account +class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController before_action :check_authorization before_action :fetch_webhook, only: [:update, :destroy] def index - @webhooks = current_account.webhooks + @webhooks = Current.account.webhooks end def create - @webhook = current_account.webhooks.new(webhook_params) + @webhook = Current.account.webhooks.new(webhook_params) @webhook.save! end @@ -28,7 +27,7 @@ class Api::V1::Accounts::WebhooksController < Api::BaseController end def fetch_webhook - @webhook = current_account.webhooks.find(params[:id]) + @webhook = Current.account.webhooks.find(params[:id]) end def check_authorization diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb similarity index 91% rename from app/controllers/api/v1/accounts/accounts_controller.rb rename to app/controllers/api/v1/accounts_controller.rb index adb8bf11c..3fb0509e3 100644 --- a/app/controllers/api/v1/accounts/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,8 +1,8 @@ -class Api::V1::Accounts::AccountsController < Api::BaseController +class Api::V1::AccountsController < Api::BaseController include AuthHelper skip_before_action :verify_authenticity_token, only: [:create] - skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, + skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception, only: [:create], raise: false before_action :check_signup_enabled, only: [:create] before_action :fetch_account, except: [:create] diff --git a/app/controllers/api/v1/agent_bots_controller.rb b/app/controllers/api/v1/agent_bots_controller.rb index 4c17fd4f4..a82f71bb9 100644 --- a/app/controllers/api/v1/agent_bots_controller.rb +++ b/app/controllers/api/v1/agent_bots_controller.rb @@ -1,6 +1,5 @@ class Api::V1::AgentBotsController < Api::BaseController skip_before_action :authenticate_user! - skip_before_action :check_subscription def index render json: AgentBot.all diff --git a/app/controllers/api/v1/integrations/webhooks_controller.rb b/app/controllers/api/v1/integrations/webhooks_controller.rb new file mode 100644 index 000000000..94373c92e --- /dev/null +++ b/app/controllers/api/v1/integrations/webhooks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index bd415ffeb..38f1a9b3b 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -16,6 +16,6 @@ class Api::V1::ProfilesController < Api::BaseController end 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 diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb index 5db576000..b2c4949df 100644 --- a/app/controllers/api/v1/webhooks_controller.rb +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -1,18 +1,6 @@ class Api::V1::WebhooksController < ApplicationController skip_before_action :authenticate_user!, raise: false 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 render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" } @@ -34,16 +22,6 @@ class Api::V1::WebhooksController < ApplicationController 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 @twitter_consumer ||= ::Webhooks::Twitter.new(params) end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 86bffb710..45cf3fc4f 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -1,4 +1,7 @@ class Api::V1::Widget::BaseController < ApplicationController + before_action :set_web_widget + before_action :set_contact + private def conversation diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index b7ac793e7..2a2b184fb 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -1,7 +1,4 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController - before_action :set_web_widget - before_action :set_contact - def update contact_identify_action = ContactIdentifyAction.new( contact: @contact, diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index cdfbd9d4a..c8d3b64a7 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -1,12 +1,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController include Events::Types - before_action :set_web_widget - before_action :set_contact def index @conversation = conversation 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 head :ok && return if conversation.nil? diff --git a/app/controllers/api/v1/widget/events_controller.rb b/app/controllers/api/v1/widget/events_controller.rb index faa44994c..ea5326d6f 100644 --- a/app/controllers/api/v1/widget/events_controller.rb +++ b/app/controllers/api/v1/widget/events_controller.rb @@ -1,15 +1,20 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController include Events::Types - before_action :set_web_widget - before_action :set_contact def create - Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox) + Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, event_info: event_info) head :no_content end private + def event_info + { + widget_language: params[:locale], + browser_language: browser.accept_language.first&.code + } + end + def permitted_params params.permit(:name, :website_token) end diff --git a/app/controllers/api/v1/widget/inbox_members_controller.rb b/app/controllers/api/v1/widget/inbox_members_controller.rb index d978a9b6e..da7d6256b 100644 --- a/app/controllers/api/v1/widget/inbox_members_controller.rb +++ b/app/controllers/api/v1/widget/inbox_members_controller.rb @@ -1,5 +1,5 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController - before_action :set_web_widget + skip_before_action :set_contact def index @inbox_members = @web_widget.inbox.inbox_members.includes(:user) diff --git a/app/controllers/api/v1/widget/labels_controller.rb b/app/controllers/api/v1/widget/labels_controller.rb index efe84f5e3..e8e409244 100644 --- a/app/controllers/api/v1/widget/labels_controller.rb +++ b/app/controllers/api/v1/widget/labels_controller.rb @@ -1,7 +1,4 @@ class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController - before_action :set_web_widget - before_action :set_contact - def create conversation.label_list.add(permitted_params[:label]) conversation.save! diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index a65e01f9c..42cd313e2 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -1,6 +1,4 @@ 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_message, only: [:update] @@ -47,7 +45,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def message_params { account_id: conversation.account_id, - contact_id: @contact.id, + sender: @contact, content: permitted_params[:message][:content], inbox_id: conversation.inbox_id, message_type: :incoming diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index 6aa0355a4..c4f563d5d 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -1,6 +1,6 @@ -class Api::V2::Accounts::ReportsController < Api::BaseController +class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController def account - builder = V2::ReportBuilder.new(current_account, account_report_params) + builder = V2::ReportBuilder.new(Current.account, account_report_params) data = builder.build render json: data end @@ -29,7 +29,7 @@ class Api::V2::Accounts::ReportsController < Api::BaseController end def account_summary_metrics - builder = V2::ReportBuilder.new(current_account, account_summary_params) + builder = V2::ReportBuilder.new(Current.account, account_summary_params) builder.summary end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 3d7f59f0c..a7764cbc9 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -1,6 +1,5 @@ class ApiController < ApplicationController skip_before_action :set_current_user, only: [:index] - skip_before_action :check_subscription, only: [:index] def index render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ae3e5e857..561a797c3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,6 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :null_session before_action :set_current_user, unless: :devise_controller? - before_action :check_subscription, unless: :devise_controller? around_action :handle_with_exception, unless: :devise_controller? # after_action :verify_authorized @@ -13,40 +12,6 @@ class ApplicationController < ActionController::Base 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 yield rescue ActiveRecord::RecordNotFound => e @@ -65,7 +30,7 @@ class ApplicationController < ActionController::Base end def current_subscription - @subscription ||= current_account.subscription + @subscription ||= Current.account.subscription end def render_unauthorized(message) @@ -94,16 +59,20 @@ class ApplicationController < ActionController::Base render json: exception.to_hash, status: exception.http_status end - def check_subscription - # This block is left over from the initial version of chatwoot - # We might reuse this later in the hosted version of chatwoot. - return if !ENV['BILLING_ENABLED'] || !current_user + def locale_from_params + I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil + end - 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 + 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 def pundit_user diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 4e784ac60..c396de4ce 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -4,6 +4,7 @@ class WidgetsController < ActionController::Base before_action :set_token before_action :set_contact before_action :build_contact + after_action :allow_iframe_requests def index; end @@ -50,4 +51,8 @@ class WidgetsController < ActionController::Base def permitted_params params.permit(:website_token, :cw_conversation) end + + def allow_iframe_requests + response.headers.delete('X-Frame-Options') + end end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 64ea55be3..c24b633aa 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -9,8 +9,7 @@ class AsyncDispatcher < BaseDispatcher end def listeners - listeners = [EventListener.instance, WebhookListener.instance] - listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED'] + listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance] listeners end end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index fcc0daf98..495bddf63 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -62,7 +62,7 @@ class ConversationFinder def find_all_conversations @conversations = current_account.conversations.includes( - :assignee, :contact, :inbox + :assignee, :inbox, contact: [:avatar_attachment] ).where(inbox_id: @inbox_ids) end diff --git a/app/helpers/api/v1/subscriptions_helper.rb b/app/helpers/api/v1/subscriptions_helper.rb deleted file mode 100644 index 2e7177559..000000000 --- a/app/helpers/api/v1/subscriptions_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Api::V1::SubscriptionsHelper -end diff --git a/app/javascript/dashboard/api/ApiClient.js b/app/javascript/dashboard/api/ApiClient.js index 9199109a1..097caa906 100644 --- a/app/javascript/dashboard/api/ApiClient.js +++ b/app/javascript/dashboard/api/ApiClient.js @@ -10,6 +10,10 @@ class ApiClient { } get url() { + return `${this.baseUrl()}/${this.resource}`; + } + + baseUrl() { let url = this.apiVersion; if (this.options.accountScoped) { const isInsideAccountScopedURLs = window.location.pathname.includes( @@ -21,7 +25,8 @@ class ApiClient { url = `${url}/accounts/${accountId}`; } } - return `${url}/${this.resource}`; + + return url; } get() { diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index f247089cd..42d1fbc41 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -118,21 +118,18 @@ export default { return axios.post(urlData.url, { email }); }, - profileUpdate({ name, email, password, password_confirmation, avatar }) { + profileUpdate({ password, password_confirmation, ...profileAttributes }) { const formData = new FormData(); - if (name) { - formData.append('profile[name]', name); - } - if (email) { - formData.append('profile[email]', email); - } + Object.keys(profileAttributes).forEach(key => { + const value = profileAttributes[key]; + if (value) { + formData.append(`profile[${key}]`, value); + } + }); if (password && password_confirmation) { formData.append('profile[password]', password); formData.append('profile[password_confirmation]', password_confirmation); } - if (avatar) { - formData.append('profile[avatar]', avatar); - } return axios.put(endPoints('profileUpdate').url, formData); }, }; diff --git a/app/javascript/dashboard/api/billing.js b/app/javascript/dashboard/api/billing.js deleted file mode 100644 index 6bc076640..000000000 --- a/app/javascript/dashboard/api/billing.js +++ /dev/null @@ -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; - }, -}; diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js index e53885b4a..2e86cb927 100644 --- a/app/javascript/dashboard/api/channel/fbChannel.js +++ b/app/javascript/dashboard/api/channel/fbChannel.js @@ -6,20 +6,6 @@ class FBChannel extends ApiClient { 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) { return axios.post( `${this.url.replace(this.resource, '')}callbacks/register_facebook_page`, diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 53c669eb6..8df609f70 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -33,14 +33,6 @@ const endPoints = { }, params: { omniauth_token: '' }, }, - - subscriptions: { - get() { - return { - url: '/api/v1/subscriptions', - }; - }, - }, }; export default page => { diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 9a50ef1c8..3f1b2e199 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -6,13 +6,14 @@ class ConversationApi extends ApiClient { super('conversations', { accountScoped: true }); } - get({ inboxId, status, assigneeType, page }) { + get({ inboxId, status, assigneeType, page, labels }) { return axios.get(this.url, { params: { inbox_id: inboxId, status, assignee_type: assigneeType, page, + labels, }, }); } @@ -43,6 +44,17 @@ class ConversationApi extends ApiClient { mute(conversationId) { 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(); diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js new file mode 100644 index 000000000..587c94d43 --- /dev/null +++ b/app/javascript/dashboard/api/integrations.js @@ -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(); diff --git a/app/javascript/dashboard/api/labels.js b/app/javascript/dashboard/api/labels.js new file mode 100644 index 000000000..8a088e840 --- /dev/null +++ b/app/javascript/dashboard/api/labels.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class LabelsAPI extends ApiClient { + constructor() { + super('labels', { accountScoped: true }); + } +} + +export default new LabelsAPI(); diff --git a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js index 63ae1492d..03f1cb649 100644 --- a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js +++ b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js @@ -9,7 +9,5 @@ describe('#FBChannel', () => { expect(fbChannel).toHaveProperty('create'); expect(fbChannel).toHaveProperty('update'); expect(fbChannel).toHaveProperty('delete'); - expect(fbChannel).toHaveProperty('markSeen'); - expect(fbChannel).toHaveProperty('toggleTyping'); }); }); diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index 1021c0409..2cc89ccc8 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -1,14 +1,14 @@ -import agents from '../contacts'; +import contacts from '../contacts'; import ApiClient from '../ApiClient'; describe('#ContactsAPI', () => { it('creates correct instance', () => { - expect(agents).toBeInstanceOf(ApiClient); - expect(agents).toHaveProperty('get'); - expect(agents).toHaveProperty('show'); - expect(agents).toHaveProperty('create'); - expect(agents).toHaveProperty('update'); - expect(agents).toHaveProperty('delete'); - expect(agents).toHaveProperty('getConversations'); + expect(contacts).toBeInstanceOf(ApiClient); + expect(contacts).toHaveProperty('get'); + expect(contacts).toHaveProperty('show'); + expect(contacts).toHaveProperty('create'); + expect(contacts).toHaveProperty('update'); + expect(contacts).toHaveProperty('delete'); + expect(contacts).toHaveProperty('getConversations'); }); }); diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js new file mode 100644 index 000000000..4eceba728 --- /dev/null +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -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'); + }); +}); diff --git a/app/javascript/dashboard/api/specs/inboxes.spec.js b/app/javascript/dashboard/api/specs/inboxes.spec.js index d1a1d2683..c4927e2bb 100644 --- a/app/javascript/dashboard/api/specs/inboxes.spec.js +++ b/app/javascript/dashboard/api/specs/inboxes.spec.js @@ -1,7 +1,7 @@ import inboxes from '../inboxes'; import ApiClient from '../ApiClient'; -describe('#AgentAPI', () => { +describe('#InboxesAPI', () => { it('creates correct instance', () => { expect(inboxes).toBeInstanceOf(ApiClient); expect(inboxes).toHaveProperty('get'); diff --git a/app/javascript/dashboard/api/specs/labels.spec.js b/app/javascript/dashboard/api/specs/labels.spec.js new file mode 100644 index 000000000..f43c807c0 --- /dev/null +++ b/app/javascript/dashboard/api/specs/labels.spec.js @@ -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'); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index a544ae883..900dd4d9d 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -1,6 +1,6 @@ .button { - font-weight: $font-weight-medium; font-family: $body-font-family; + font-weight: $font-weight-medium; &.round { border-radius: 1000px; @@ -20,10 +20,11 @@ } .tooltip { - max-width: 15rem; - padding: $space-smaller $space-small; border-radius: $space-smaller; font-size: $font-size-mini; + max-width: 15rem; + padding: $space-smaller $space-small; + z-index: 9999; } code { diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 169198dae..9d3650f88 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -67,6 +67,7 @@ BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", +Tahoma, Arial, sans-serif; $body-antialiased: true; @@ -382,7 +383,7 @@ $label-color: $primary-color; $label-color-alt: $black; $label-palette: $foundation-palette; $label-font-size: $font-size-micro; -$label-padding: $space-micro $space-smaller; +$label-padding: $space-smaller $space-small; $label-radius: $space-micro; // 21. Media Object diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss index d34d30beb..ffd9e734e 100644 --- a/app/javascript/dashboard/assets/scss/_helper-classes.scss +++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss @@ -25,7 +25,7 @@ width: $space-medium; &.message { - @include elegent-shadow; + @include normal-shadow; background: $color-white; border-radius: $space-large; left: 0; diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index e4efee5ca..9bf19e1c2 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -1,6 +1,5 @@ @import '~widget/assets/scss/mixins'; -$elegant-shadow-color: rgba(49, 49, 93, 0.15); $spinner-before-border-color: rgba(255, 255, 255, 0.7); //borders @@ -141,12 +140,8 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); overflow-y: auto; } -@mixin elegent-shadow() { - box-shadow: 0 10px 25px 0 $elegant-shadow-color; -} - @mixin elegant-card() { - @include elegent-shadow; + @include normal-shadow; border-radius: $space-small; } @@ -194,28 +189,42 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); border-bottom: $size solid $color; border-left: $size solid transparent; border-right: $size solid transparent; - } @else if $direction == 'right' { + } + + @else if $direction == 'right' { border-bottom: $size solid transparent; border-left: $size solid $color; border-top: $size solid transparent; - } @else if $direction == 'bottom' { + } + + @else if $direction == 'bottom' { border-left: $size solid transparent; border-right: $size solid transparent; border-top: $size solid $color; - } @else if $direction == 'left' { + } + + @else if $direction == 'left' { border-bottom: $size solid transparent; border-right: $size solid $color; border-top: $size solid transparent; - } @else if $direction == 'top-left' { + } + + @else if $direction == 'top-left' { border-right: $size solid transparent; border-top: $size solid $color; - } @else if $direction == 'top-right' { + } + + @else if $direction == 'top-right' { border-left: $size solid transparent; border-top: $size solid $color; - } @else if $direction == 'bottom-left' { + } + + @else if $direction == 'bottom-left' { border-bottom: $size solid $color; border-right: $size solid transparent; - } @else if $direction == 'bottom-right' { + } + + @else if $direction == 'bottom-right' { border-bottom: $size solid $color; border-left: $size solid transparent; } diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 947204ca6..7f5ef39c9 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -3,7 +3,6 @@ @import 'animations'; @import 'foundation-custom'; -@import 'widgets/billing'; @import 'widgets/buttons'; @import 'widgets/conv-header'; @import 'widgets/conversation-card'; diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss index e791f4cd7..b43bbb4bc 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss @@ -3,16 +3,17 @@ background: $color-white; border: 1px solid $color-border; border-radius: $space-smaller; + margin-bottom: $space-normal; padding: $space-normal; .integration--image { display: flex; margin-right: $space-normal; - width: 8rem; + width: 10rem; img { - max-width: 8rem; - padding: $space-small; + max-width: 100%; + padding: $space-medium; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_billing.scss b/app/javascript/dashboard/assets/scss/widgets/_billing.scss deleted file mode 100644 index 0fd6d45ff..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_billing.scss +++ /dev/null @@ -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; - } - } -} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index 6de396353..e1aef9307 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -66,6 +66,8 @@ } .button.resolve--button { + @include flex-align($x: center, $y: middle); + width: 13.2rem; >.icon { diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss index 498a5e9d2..95fcf7699 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss @@ -78,6 +78,11 @@ font-size: $font-size-mini; vertical-align: top; } + + .message-from-agent { + color: $color-gray; + font-size: $font-size-mini; + } } .conversation--meta { @@ -120,11 +125,11 @@ } .conversation--message { - font-weight: $font-weight-medium; + font-weight: $font-weight-bold; } .conversation--user { - font-weight: $font-weight-medium; + font-weight: $font-weight-bold; } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 6aef3e3f9..8c615a016 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -26,6 +26,7 @@ .link { color: $color-white; + text-decoration: underline; } } @@ -308,7 +309,7 @@ &.is-private { background: lighten($warning-color, 32%); - border: 1px solid $color-border; + border: 1px solid lighten($warning-color, 15%); color: $color-heading; padding-right: $space-large; position: relative; diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 0ae8ecf21..8c4e656c4 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -67,6 +67,10 @@ font-size: $font-size-small; } + .content { + @include padding($space-large); + } + form { @include padding($space-large); align-self: center; diff --git a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss index d0bab2074..29ba3c66c 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reply-box.scss @@ -1,10 +1,15 @@ .reply-box { - @include elegant-card; + @include light-shadow; border-bottom: 0; + border-radius: $space-small; margin: $space-normal; margin-top: 0; - max-height: $space-jumbo * 2; - transition: height 2s $ease-in-out-cubic; + max-height: $space-mega * 3; + transition: box-shadow .35s $ease-in-out-cubic, height 2s $ease-in-out-cubic; + + &.is-focused { + @include normal-shadow; + } .reply-box__top { @include flex; @@ -42,7 +47,7 @@ &.is-private { background: lighten($warning-color, 38%); - >input { + > input { background: lighten($warning-color, 38%); } } @@ -58,7 +63,7 @@ } } - .file-uploads>label { + .file-uploads > label { cursor: pointer; } @@ -68,13 +73,14 @@ padding: 0 $space-small; } - >textarea { + > textarea { @include ghost-input(); @include margin(0); background: transparent; // Override min-height : 50px in foundation // - min-height: 1rem; + max-height: $space-mega * 2.4; + min-height: 4rem; resize: none; } } diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 59012d516..5ca82a407 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -3,7 +3,7 @@

- {{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }} + {{ pageTitle }}

@@ -15,14 +15,15 @@ @chatTabChange="updateAssigneeTab" /> -

+

{{ $t('CHAT_LIST.LIST.404') }}

@@ -40,7 +41,7 @@

/* eslint-env browser */ /* eslint no-console: 0 */ +/* global bus */ import { mapGetters } from 'vuex'; import ChatFilter from './widgets/conversation/ChatFilter'; @@ -71,7 +73,16 @@ export default { ChatFilter, }, mixins: [timeMixin, conversationMixin], - props: ['conversationInbox'], + props: { + conversationInbox: { + type: [String, Number], + default: 0, + }, + label: { + type: String, + default: '', + }, + }, data() { return { activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, @@ -87,14 +98,17 @@ export default { chatListLoading: 'getChatListLoadingStatus', currentUserID: 'getCurrentUserID', activeInbox: 'getSelectedInbox', - convStats: 'getConvTabStats', + conversationStats: 'conversationStats/getStats', }), assigneeTabItems() { - return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({ - key: item.KEY, - name: item.NAME, - count: this.convStats[item.COUNT_KEY] || 0, - })); + return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => { + const count = this.conversationStats[item.COUNT_KEY] || 0; + return { + key: item.KEY, + name: item.NAME, + count, + }; + }); }, inbox() { return this.$store.getters['inboxes/getInbox'](this.activeInbox); @@ -109,16 +123,61 @@ export default { 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: { conversationInbox() { this.resetAndFetchData(); }, + label() { + this.resetAndFetchData(); + }, }, mounted() { this.$store.dispatch('setChatFilter', this.activeStatus); this.resetAndFetchData(); - this.$store.dispatch('agents/get'); + + bus.$on('fetch_conversation_stats', () => { + this.$store.dispatch('conversationStats/get', this.conversationFilters); + }); }, methods: { resetAndFetchData() { @@ -127,12 +186,7 @@ export default { this.fetchConversations(); }, fetchConversations() { - this.$store.dispatch('fetchAllConversations', { - inboxId: this.conversationInbox ? this.conversationInbox : undefined, - assigneeType: this.activeAssigneeTab, - status: this.activeStatus, - page: this.currentPage + 1, - }); + this.$store.dispatch('fetchAllConversations', this.conversationFilters); }, updateAssigneeTab(selectedTab) { if (this.activeAssigneeTab !== selectedTab) { @@ -148,17 +202,6 @@ export default { 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; - }, }, }; diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index 24a81ee20..4fbfa5338 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -6,6 +6,7 @@ import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; import DeleteModal from './widgets/modal/DeleteModal.vue'; import Input from './widgets/forms/Input.vue'; +import Label from './widgets/Label.vue'; import LoadingState from './widgets/LoadingState'; import Modal from './Modal'; import ModalHeader from './ModalHeader'; @@ -25,6 +26,7 @@ const WootUIKit = { DeleteModal, Input, LoadingState, + Label, Modal, ModalHeader, ReportStatsCard, diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 986232749..d6ab8a75d 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -18,21 +18,14 @@ :key="inboxSection.toState" :menu-item="inboxSection" /> +

- - - - -
- +

{{ currentUser.name }} @@ -108,7 +105,6 @@ import { mixin as clickaway } from 'vue-clickaway'; import adminMixin from '../../mixins/isAdmin'; import Auth from '../../api/auth'; import SidebarItem from './SidebarItem'; -import WootStatusBar from '../widgets/StatusBar'; import { frontendURL } from '../../helper/URLHelper'; import Thumbnail from '../widgets/Thumbnail'; import { getSidebarItems } from '../../i18n/default-sidebar'; @@ -116,7 +112,6 @@ import { getSidebarItems } from '../../i18n/default-sidebar'; export default { components: { SidebarItem, - WootStatusBar, Thumbnail, }, mixins: [clickaway, adminMixin], @@ -135,12 +130,11 @@ export default { computed: { ...mapGetters({ currentUser: 'getCurrentUser', - daysLeft: 'getTrialLeft', globalConfig: 'globalConfig/get', inboxes: 'inboxes/getInboxes', - subscriptionData: 'getSubscription', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + accountLabels: 'labels/getLabelsOnSidebar', }), sidemenuItems() { return getSidebarItems(this.accountId); @@ -160,10 +154,6 @@ export default { } } - if (!window.chatwootConfig.billingEnabled) { - menuItems = this.filterBillingRoutes(menuItems); - } - return this.filterMenuItemsByRole(menuItems); }, 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() { 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() { this.$store.dispatch('inboxes/get'); }, methods: { - filterBillingRoutes(menuItems) { - return menuItems.filter( - menuItem => !menuItem.toState.includes('billing') - ); - }, filterMenuItemsByRole(menuItems) { if (!this.currentRole) { return []; diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 35356a685..7ef2dfa88 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -36,7 +36,13 @@ v-if="computedInboxClass(child)" class="inbox-icon" :class="computedInboxClass(child)" - > + /> + + {{ child.label }}

@@ -126,8 +132,22 @@ export default { }; diff --git a/app/javascript/dashboard/components/widgets/BackButton.vue b/app/javascript/dashboard/components/widgets/BackButton.vue index 9b5085983..9bf87d131 100644 --- a/app/javascript/dashboard/components/widgets/BackButton.vue +++ b/app/javascript/dashboard/components/widgets/BackButton.vue @@ -1,14 +1,26 @@ \ No newline at end of file + diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 948bff103..bd4636fd8 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -40,10 +40,23 @@ export default { type: String, required: true, }, + enabledFeatures: { + type: Object, + required: true, + }, }, methods: { 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() { if (this.isActive(this.channel)) { diff --git a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue index 0ed5e542b..e57f63fbf 100644 --- a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue +++ b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue @@ -22,11 +22,6 @@ export default { default: wootConstants.ASSIGNEE_TYPE.ME, }, }, - data() { - return { - tabsIndex: wootConstants.ASSIGNEE_TYPE.ME, - }; - }, computed: { activeTabIndex() { return this.items.findIndex(item => item.key === this.activeTab); diff --git a/app/javascript/dashboard/components/widgets/Label.vue b/app/javascript/dashboard/components/widgets/Label.vue new file mode 100644 index 000000000..9b56a4c91 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/Label.vue @@ -0,0 +1,91 @@ + + + + diff --git a/app/javascript/dashboard/components/widgets/StatusBar.vue b/app/javascript/dashboard/components/widgets/StatusBar.vue deleted file mode 100644 index 902c2515e..000000000 --- a/app/javascript/dashboard/components/widgets/StatusBar.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 25b707c04..2d1c3e200 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -21,11 +21,6 @@ :style="badgeStyle" src="~dashboard/assets/images/fb-badge.png" /> -
- +
@@ -31,14 +42,18 @@ export default { padding-bottom: 0; } - .conv-details--item__icon { - padding-right: $space-smaller; - } - .conv-details--item__label { - font-weight: $font-weight-medium; - margin-bottom: $space-micro; + align-items: center; + display: flex; font-size: $font-size-small; + font-weight: $font-weight-medium; + justify-content: space-between; + margin-bottom: $space-micro; + + button { + cursor: pointer; + color: $color-body; + } } .conv-details--item__value { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index d653eb1cf..e9ed6b71b 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -2,13 +2,13 @@
- +
@@ -107,7 +107,7 @@ import { mapGetters } from 'vuex'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import ContactConversations from './ContactConversations.vue'; import ContactDetailsItem from './ContactDetailsItem.vue'; -import ConversationLabels from './ConversationLabels.vue'; +import ConversationLabels from './labels/LabelBox.vue'; export default { components: { @@ -158,6 +158,9 @@ export default { } = this.browser; return `${platformName || ''} ${platformVersion || ''}`; }, + channelType() { + return this.currentChat.meta?.channel; + }, contactId() { return this.currentChat.meta?.sender?.id; }, @@ -168,12 +171,15 @@ export default { watch: { conversationId(newConversationId, prevConversationId) { if (newConversationId && newConversationId !== prevConversationId) { - this.$store.dispatch('contacts/show', { id: this.contactId }); + this.getContactDetails(); } }, + contactId() { + this.getContactDetails(); + }, }, mounted() { - this.$store.dispatch('contacts/show', { id: this.contactId }); + this.getContactDetails(); }, methods: { onPanelToggle() { @@ -182,6 +188,11 @@ export default { mute() { this.$store.dispatch('muteConversation', this.conversationId); }, + getContactDetails() { + if (this.contactId) { + this.$store.dispatch('contacts/show', { id: this.contactId }); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue deleted file mode 100644 index 728815a0e..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationLabels.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue index c51053c19..e073965f6 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ConversationView.vue @@ -1,6 +1,6 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue new file mode 100644 index 000000000..cdab7ceee --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue @@ -0,0 +1,114 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue new file mode 100644 index 000000000..3185f17b0 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue @@ -0,0 +1,70 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue index 980f16b05..6f2ce7f40 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue @@ -82,16 +82,16 @@
diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/EditLabel.vue b/app/javascript/dashboard/routes/dashboard/settings/labels/EditLabel.vue new file mode 100644 index 000000000..43e75de64 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/EditLabel.vue @@ -0,0 +1,129 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue new file mode 100644 index 000000000..a28362c5a --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/Index.vue @@ -0,0 +1,201 @@ + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js new file mode 100644 index 000000000..eef8d301d --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/labels.routes.js @@ -0,0 +1,31 @@ +import SettingsContent from '../Wrapper'; +import Index from './Index'; +import { frontendURL } from '../../../../helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('accounts/:accountId/settings/labels'), + component: SettingsContent, + props: { + headerTitle: 'LABEL_MGMT.HEADER', + icon: 'ion-pricetags', + showNewButton: false, + }, + children: [ + { + path: '', + name: 'labels_wrapper', + roles: ['administrator'], + redirect: 'list', + }, + { + path: 'list', + name: 'labels_list', + roles: ['administrator'], + component: Index, + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validations.spec.js b/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validations.spec.js new file mode 100644 index 000000000..47c8c21df --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/specs/validations.spec.js @@ -0,0 +1,10 @@ +import { validLabelCharacters } from '../validations'; + +describe('#validLabelCharacters', () => { + it('validates the label', () => { + expect(validLabelCharacters('')).toEqual(false); + expect(validLabelCharacters('str str')).toEqual(false); + expect(validLabelCharacters('str_str')).toEqual(true); + expect(validLabelCharacters('str-str')).toEqual(true); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/settings/labels/validations.js b/app/javascript/dashboard/routes/dashboard/settings/labels/validations.js new file mode 100644 index 000000000..0a2373a06 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/labels/validations.js @@ -0,0 +1,16 @@ +import { required, minLength } from 'vuelidate/lib/validators'; + +export const validLabelCharacters = (str = '') => !!str && !str.includes(' '); + +export default { + title: { + required, + minLength: minLength(2), + validLabelCharacters, + }, + description: {}, + color: { + required, + }, + showOnSidebar: {}, +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 03944cd2a..eaf40b077 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -38,6 +38,19 @@ {{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }} +
@@ -99,16 +112,17 @@ diff --git a/app/javascript/shared/helpers/BaseActionCableConnector.js b/app/javascript/shared/helpers/BaseActionCableConnector.js index c601d4164..ed0c1bb65 100644 --- a/app/javascript/shared/helpers/BaseActionCableConnector.js +++ b/app/javascript/shared/helpers/BaseActionCableConnector.js @@ -1,20 +1,31 @@ import { createConsumer } from '@rails/actioncable'; +const PRESENCE_INTERVAL = 60000; + class BaseActionCableConnector { constructor(app, pubsubToken) { this.consumer = createConsumer(); - this.consumer.subscriptions.create( + this.subscription = this.consumer.subscriptions.create( { channel: 'RoomChannel', pubsub_token: pubsubToken, + account_id: app.$store.getters.getCurrentAccountId, + user_id: app.$store.getters.getCurrentUserID, }, { + updatePresence() { + this.perform('update_presence'); + }, received: this.onReceived, } ); this.app = app; this.events = {}; this.isAValidEvent = () => true; + + setInterval(() => { + this.subscription.updatePresence(); + }, PRESENCE_INTERVAL); } disconnect() { diff --git a/app/javascript/shared/helpers/vuex/mutationHelpers.js b/app/javascript/shared/helpers/vuex/mutationHelpers.js index a965c8f8a..9822c0224 100644 --- a/app/javascript/shared/helpers/vuex/mutationHelpers.js +++ b/app/javascript/shared/helpers/vuex/mutationHelpers.js @@ -25,6 +25,26 @@ export const update = (state, data) => { }); }; +/* when you don't want to overwrite the whole object */ +export const updateAttributes = (state, data) => { + state.records.forEach((element, index) => { + if (element.id === data.id) { + Vue.set(state.records, index, { ...state.records[index], ...data }); + } + }); +}; + +export const updatePresence = (state, data) => { + state.records.forEach((element, index) => { + const availabilityStatus = data[element.id]; + if (availabilityStatus) { + Vue.set(state.records[index], 'availability_status', availabilityStatus); + } else { + Vue.delete(state.records[index], 'availability_status'); + } + }); +}; + export const destroy = (state, id) => { state.records = state.records.filter(record => record.id !== id); }; diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index e5414a1ac..bdbd1388c 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -1,22 +1,57 @@ diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index d70aaf342..9933e529a 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -30,10 +30,18 @@ const toggleTyping = async ({ typingStatus }) => { ); }; +const setUserLastSeenAt = async ({ lastSeen }) => { + return API.post( + `/api/v1/widget/conversations/update_last_seen${window.location.search}`, + { user_last_seen_at: lastSeen } + ); +}; + export { sendMessageAPI, getConversationAPI, getMessagesAPI, sendAttachmentAPI, toggleTyping, + setUserLastSeenAt, }; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index ef51af907..64432402e 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -1,15 +1,8 @@ -import Vue from 'vue'; +import { buildSearchParamsWithLocale } from '../helpers/urlParamsHelper'; const sendMessage = content => { - const locale = Vue.config.lang; const refererURL = window.refererURL || ''; - let search = window.location.search; - if (search) { - search = `${search}&locale=${locale}`; - } else { - search = `?locale=${locale}`; - } - + const search = buildSearchParamsWithLocale(window.location.search); return { url: `/api/v1/widget/messages${search}`, params: { diff --git a/app/javascript/widget/api/events.js b/app/javascript/widget/api/events.js index 61c0c0db2..0c8fcfb23 100644 --- a/app/javascript/widget/api/events.js +++ b/app/javascript/widget/api/events.js @@ -1,7 +1,9 @@ import { API } from 'widget/helpers/axios'; +import { buildSearchParamsWithLocale } from '../helpers/urlParamsHelper'; export default { create(name) { - return API.post(`/api/v1/widget/events${window.location.search}`, { name }); + const search = buildSearchParamsWithLocale(window.location.search); + return API.post(`/api/v1/widget/events${search}`, { name }); }, }; diff --git a/app/javascript/widget/assets/scss/_variables.scss b/app/javascript/widget/assets/scss/_variables.scss index 5d4154315..f7f3837a3 100755 --- a/app/javascript/widget/assets/scss/_variables.scss +++ b/app/javascript/widget/assets/scss/_variables.scss @@ -60,6 +60,11 @@ $color-body: #3c4858; $color-heading: #1f2d3d; $color-error: #ff382d; +// Color-palettes + +$color-primary-light: #c7e3ff; +$color-primary-dark: darken($color-woot, 20%); + // Thumbnail $thumbnail-radius: 4rem; @@ -99,6 +104,7 @@ BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", +Tahoma, Arial, sans-serif; $ionicons-font-path: '~ionicons/fonts'; @@ -109,3 +115,7 @@ $spinkit-size: 1.6rem !default; // Break points $break-point-medium: 667px; + +// Timing functions + +$ease-in-cubic: cubic-bezier(.17, .67, .83, .67); diff --git a/app/javascript/widget/assets/scss/sdk.js b/app/javascript/widget/assets/scss/sdk.js index bee0ca26e..558812c57 100644 --- a/app/javascript/widget/assets/scss/sdk.js +++ b/app/javascript/widget/assets/scss/sdk.js @@ -11,6 +11,18 @@ export const SDK_CSS = ` .woot-widget-holder { transition-duration: 0.5s, 0.5s; } +.woot-widget-holder.has-unread-view { + box-shadow: none !important; + -moz-box-shadow: none !important; + -o-box-shadow: none !important; + -webkit-box-shadow: none !important; + -o-border-radius: 0 !important; + -moz-border-radius: 0 !important; + -webkit-border-radius: 0 !important; + border-radius: 0 !important; + bottom: 94px; +} + .woot-widget-holder iframe { width: 100% !important; height: 100% !important; @@ -94,7 +106,7 @@ export const SDK_CSS = ` .woot-widget-holder { visibility: hidden !important; z-index: -1 !important; opacity: 0; - bottom: 60px; + bottom: -20000px; } @media only screen and (max-width: 667px) { diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue index 1062614da..2676839a6 100755 --- a/app/javascript/widget/components/AgentMessageBubble.vue +++ b/app/javascript/widget/components/AgentMessageBubble.vue @@ -1,5 +1,5 @@