Compare commits

...
97 Commits
Author SHA1 Message Date
Pranav Raj Sreepuram bf80a37cfb Merge branch 'hotfix/1.2.4' 2020-03-22 20:41:51 +05:30
Pranav Raj SandGitHub a94f14ca22 Bug: Disable logging to file on asset precompile (#637) 2020-03-22 20:38:49 +05:30
Pranav Raj SandGitHub 1c4566df66 Bug: Migrate messages while merging contact (#624)
Bug: Migrate messages while merging contact
2020-03-19 01:52:38 +05:30
Sojan 9d170fc727 Merge branch 'hotfix/1.2.3' 2020-03-18 01:53:13 +05:30
Sojan 8d65e7db95 HotFix: Tune the chatwoot logs
Fixes : #616
2020-03-18 01:40:34 +05:30
Pranav Raj Sreepuram 91bccd3c6d Merge branch 'hotfix/1.2.2' 2020-03-09 10:00:42 +05:30
Pranav Raj Sreepuram ef6c695bcb Chore: Add additional_attributes to conversation seed 2020-03-09 10:00:11 +05:30
Pranav Raj Sreepuram 1273ea64d3 Merge branch 'release/1.2.1' 2020-03-08 22:15:04 +05:30
Pranav Raj SandGitHub 2a4fb7b056 Bug: Fix conversation not loading from the links in email (#602)
Bug: Load conversation from links
2020-03-08 22:08:25 +05:30
Sojan JoseandGitHub a8ac048716 Bug: Fix duplicate messages from Twitter DM and tweets (#599)
* Bug: Fix duplicate messages from Twitter DM and tweets
2020-03-06 01:47:37 +05:30
Sony MathewandGitHub fabc6c87af Chore: User Friendly URLs on the swagger docs (#590)
Made the swagger documentation urls short and readable.
2020-03-04 00:41:21 +05:30
Pranav Raj SandGitHub f76ba889dd Docs: Add webhook documentation (#588) 2020-03-03 18:52:30 +05:30
Pranav Raj SandGitHub 97ad512c21 Security: Upgrade puma to 4.3.3 (#586) 2020-03-03 13:00:54 +05:30
Nithin David ThomasandGitHub dfdb68e1c3 Enhancement: Improve UI on user preferences page (#584) 2020-03-03 12:39:05 +05:30
Sojan c772c0af03 Merge branch 'release/1.2.0' 2020-03-01 19:33:41 +05:30
Sojan JoseandGitHub cda65ea339 Feature: Conversation creation email notifications (#576)
* Clean up the mailers

* Disable assignment mailer if setting is turned off

* Email notifications on conversation create

* Specs
2020-03-01 19:06:13 +05:30
Pranav Raj SandGitHub d6237dfc59 Feature: Add UI to update email notification preferences (#579) 2020-03-01 17:47:08 +05:30
Pranav Raj SandGitHub 629c73d3ce Fix onBackDropClick (#578) 2020-03-01 11:23:55 +05:30
Tim LangeandGitHub bbf01037c6 Chore: Added tests for conversations messages controller (#575) 2020-03-01 04:56:30 +05:30
7f26b34b15 Feature: Add new notification settings for user (#569)
Added new notification settings API for user 

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-02-29 20:41:09 +05:30
dependabot[bot]andGitHub bbd9968d4b Chore(deps): Bump puma from 4.3.1 to 4.3.2 (#573)
Bumps [puma](https://github.com/puma/puma) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v4.3.1...v4.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-29 18:06:27 +05:30
c119c6577b Feature: As a admin, I should be able to add webhooks to account (#572)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-29 17:43:49 +05:30
Pranav Raj SandGitHub e8cf59c661 Feature: Alert widget user when message is received from agent (#571) 2020-02-29 11:27:02 +05:30
b05f843790 Chore: Making Account Signup Optional (#563)
Introduce new environment variable that lets you control account signups
ENABLE_ACCOUNT_SIGNUP :( true | false | api_only )
Fixes: #406

Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-29 11:20:33 +05:30
Sony MathewandGitHub 0aad99ff0b Chore: Update the source of json_refs gem (#565) (#566)
At the time of using, `json_refs` gem did not have the ability to recursively resolve the references. So we had to fork this into one of our own and make the required changes. So this fork was used as the source for the gem.
Along with this, we had raised a PR in the original repo so as to push the recursive dereferencing changes upstream. This was merged in the gem's parent repo. So updated the source of this gem to the original parent repo source.
2020-02-27 01:19:26 +05:30
Pranav Raj SandGitHub 0740d4762f Enhancement: Paginate conversation calls in tabs (#560)
* Use conversationPage module for pagination

* Load more conversations

* Reset list if conversation status is changed

* Add specs to conversationPage

* Reset filter when page is re-mounted

* Update text

* Update text
2020-02-26 21:15:01 +05:30
Tim LangeandGitHub e5bc372a29 Chore: Added tests for callbacks_controller (#531)
* Chore: Added tests for callbacks_controller
2020-02-26 18:22:08 +05:30
74f8a35cfc Chore: Environment variable to force ssl (#561)
Fixes: #558

Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-26 11:36:22 +05:30
1f158aa193 Chore(deps): Bump nokogiri from 1.10.7 to 1.10.8 (#564)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.7 to 1.10.8.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.7...v1.10.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-26 09:59:38 +05:30
Subin T PandGitHub 7479b5db43 Feature: Support account/inbox specific webhooks (#562) 2020-02-26 09:44:24 +05:30
Sony MathewandGitHub 4d5e7b4adf Bug: Added Sidekiq to production docker compose (#555) (#557)
* Sidekiq service was missing from the production docker compose file. Added it.
2020-02-25 01:23:29 +05:30
Pranav Raj SandGitHub 55b001435b Bug: Fix yarn.lock discrepancies (#554) 2020-02-24 10:32:35 +05:30
Sony MathewandGitHub fc1fa579e9 Chore: Document Conversation APIs in swagger (#552)
* Added the documentation for the conversation APIs - 8 endpoints
* Added descriptions for the existing endpoints
* Added new resource models and updated some parts of the contact API
2020-02-24 00:51:31 +05:30
Sony MathewandGitHub c7a4550477 Updated Docker documentation
* Removed the links to docker hub
2020-02-23 23:11:11 +05:30
Nithin David ThomasandGitHub fd8e8c7c87 Enhancement: Add close button to the contact sidebar panel (#551) 2020-02-23 21:13:28 +05:30
Nithin David ThomasandGitHub 2d3212575d Enhancement: Hides available in widget interaction (#548) 2020-02-23 15:16:09 +05:30
Sony MathewandGitHub e2688d5176 Chore: Swagger Docs V1 (#546) 2020-02-23 12:54:29 +05:30
Nithin David ThomasandGitHub eea92bd0ac Bug: Add link cursor to widget close button (#544) 2020-02-22 20:20:43 +05:30
Pranav Raj SandGitHub 1261cc7783 Enhancement: Upgrade @babel/preset-env (#542) 2020-02-22 20:16:56 +05:30
a790dc640f Fix: Changed overflow scroll to overflow auto (#523)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-02-22 19:15:45 +05:30
67f4f69e8a Bug: Fix error message when existing agent email is reused for a new agent (#538)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-22 19:06:16 +05:30
Pranav Raj SandGitHub 6340eb3ae2 Security: Upgrade @rails/webpacker and jest (#540)
* Security: Upgrade @rails/webpacker and jest

* Upgrade jest from 24.9.0 to 25.1.0
* Upgrade @rails/webpacker from 4.0.7 to 4.2.2

* Update test to use 1 worker
2020-02-22 19:02:11 +05:30
Nithin David ThomasandGitHub 708d2688ff Bug: Fix URL text overflow (#537) 2020-02-22 15:32:15 +05:30
91f8b4a650 Bug: Show helper description for code in finish setup page (#532)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-02-22 14:27:04 +05:30
Nithin David ThomasandGitHub 330bf87f08 Bug : Inbox Filter indicator is removed when a conversation is selected (#535) 2020-02-22 14:24:51 +05:30
Tim LangeandGitHub bc884ae184 Chore: Added tests for dashboard_controller (#529) 2020-02-21 00:43:14 +05:30
Nithin David ThomasandGitHub f7c8358549 Bug: Hide contact panel when inboxId is not present (#530) 2020-02-20 19:48:09 +05:30
Subin T PandGitHub d36e1b52f6 Enhancement: Add event name in webhook payload (#528)
* chore: Remove unwanted twitter gem
* Enhancement: Add event name in webhook payload
fixes #528 #516 #518
2020-02-20 16:19:42 +05:30
Tim LangeandGitHub 813cdd8aec Chore: Added tests for facebook_indicators_controller (#526) 2020-02-20 12:12:19 +05:30
b7e725a7cd Fix: Fixed code font not being displayed on windows (#525)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-02-20 11:11:36 +05:30
9674e0bff4 Bug: Fix misalignment of back button (#524)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-02-20 11:02:12 +05:30
4817f4a28c Bug: Fixed color of agent link in chat widget being white (#522)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
2020-02-20 10:59:43 +05:30
Sony MathewandGitHub 04c62417fa [491] Bug fix - Sidekiq Redis Auth issue (#527)
* When the the .env file has line with REDIS_PASSWORD set as empty, the value for this in the initializers comes as an empty string "".
* Fixed this in a way that, if it's empty string, then it's taken as `nil` value so that password is skipped
2020-02-20 10:52:26 +05:30
Tim LangeandGitHub 30e5edf6dc Feature: Add ability to disable auto assignment of conversations (#513) 2020-02-19 14:40:03 +05:30
Tim LangeandGitHub e0afb84502 Chore: Removed unused functions from home_controller (#512) 2020-02-19 13:51:22 +05:30
fbefb78817 Enhancement: Update the design of inbox listing in sidebar (#511)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-19 10:30:34 +05:30
Vishnu NarayananandGitHub f00e1dcdc8 Chore: Update docker setup manual (#504)
* Chore: Update docker setup manual (#503)
2020-02-17 19:52:26 +05:30
Tim LangeandGitHub 60547b9fb3 Chore: Added tests for contacts_controller (#509)
- Tests for contacts controller
Addresses:  #37
2020-02-17 17:08:29 +05:30
Tim LangeandGitHub fc23a10a99 Chore: Added tests for conversations_controller (#508)
* Chore: Added tests for conversations_controller
* Chore: Remove get_messages function from conversations
2020-02-17 15:57:57 +05:30
Tim LangeandGitHub 44a3977f5c Chore: Added tests for inboxes_controller (#507) 2020-02-17 15:39:24 +05:30
Tim LangeandGitHub a2316d4d09 Chore: Added tests for subscriptions_controller (#483)
Added tests for subscriptions_controller
ref: #37
2020-02-17 07:51:45 +05:30
Pranav Raj SandGitHub c32d391261 Bug: Fix imports in ProfileSettings (#506) 2020-02-16 22:20:59 +05:30
Pranav Raj SandGitHub c4e2a84f65 Feature: Agent Profile Update with avatar (#449)
* Feature: Agent Profile Update with avatar
* Add Update Profile with name, avatar, email and password
2020-02-16 17:20:38 +05:30
e61ba95cf7 Feature: Add/Edit conversation labels (#488)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-16 15:46:26 +05:30
Sojan JoseandGitHub 77473dc2aa Chore: Include avatar url in sign_in response (#501)
- include avatar url in sign_in response ( fixes #500 )
- fix circle ci builds
2020-02-15 22:57:48 +05:30
Subin T PandGitHub eb5185974a Monitoring: Add Scout APM (#487)
* Add scout APM
2020-02-15 04:22:36 +05:30
Subin T PandGitHub 919261d843 Feature: Webhooks (#489) 2020-02-14 23:19:17 +05:30
sony-mathew 79a847aeab Merge branch 'hotfix/1.1.3' into develop - Bug Fix : Adjusted the connection pool size and concurrency for Sidekiq 2020-02-11 22:59:07 +05:30
sony-mathew 301b1829bc Merge branch 'hotfix/1.1.3' - Bug Fix : Adjusted the connection pool size and concurrency for Sidekiq 2020-02-11 22:58:43 +05:30
sony-mathew 6645d7f80b Bug Fix : Adjusted the connection pool size and concurrency for Sidekiq 2020-02-11 22:58:19 +05:30
sony-mathew baf1a6e8da Merge branch 'hotfix/1.1.3' into develop - Bug Fix : Increased the connection pool size and concurrency for Sidekiq 2020-02-11 21:52:37 +05:30
sony-mathew 1e955455f0 Merge branch 'hotfix/1.1.3' Bugfix : Increased sidekiq connection pool and concurrency 2020-02-11 21:51:46 +05:30
sony-mathew fdbad7c5d8 Bug Fix : Increased the connection pool size and concurrency for Sidekiq 2020-02-11 21:50:38 +05:30
Sojan 2023d5d42b Revert "Updates sidebar on dashboard"
This reverts commit 6186d14593.
2020-02-11 19:34:42 +05:30
Nithin David 6186d14593 Updates sidebar on dashboard 2020-02-11 19:21:55 +05:30
30f4c08143 Feature: SignIn with Twitter (#479)
* Add Twitter SignIn flow

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-02-11 14:27:38 +05:30
Pranav Raj SandGitHub 272c481464 Feature: Create conversations from Tweets (#470)
* Feature: Add tweets to conversations
2020-02-09 15:47:48 +05:30
Tim LangeandGitHub 3465ebefd1 Chore: Added tests for assignments_controller (#482) 2020-02-08 23:48:00 +05:30
Tim LangeandGitHub b9836309fb Chore: Added tests for agents_controller (#481) 2020-02-08 23:05:41 +05:30
Tim LangeandGitHub 6c46891ee1 Chore: Remove unnessecary check if object exists (#480) 2020-02-08 22:38:37 +05:30
Tim LangeandGitHub 67e41092e6 Chore: Added tests for inbox_members (#478) 2020-02-08 13:21:41 +05:30
Tim LangeandGitHub 1d269f4e8d Chore: Added tests for canned_responses (#477) 2020-02-08 11:58:40 +05:30
Tim LangeandGitHub 37289fbd63 Chore: Increase test coverage for conversations controller (#476) 2020-02-07 21:43:51 +05:30
Tim LangeandGitHub a30a8867a3 Chore: Increase test coverage for accounts controller (#475) 2020-02-07 16:41:49 +05:30
Tim LangeandGitHub 5210d201b6 Chore: Increase test coverage for widgets controller (#473) 2020-02-07 13:14:07 +05:30
Vishnu NarayananandGitHub 3eb5b3a0c1 docs: Update contribution guidelines (#471) (#474) 2020-02-07 12:05:09 +05:30
Tim LangeandGitHub 3fa7c605c3 Chore: Increase test coverage for widget messages (#472) 2020-02-07 00:51:47 +05:30
Pranav Raj SandGitHub 7b1fdc5fcd BugFix: Filter duplicate DMs in Twitter Integration (#468)
BugFix: Filter duplicate DMs in Twitter Integration
2020-02-05 18:50:37 +05:30
Pranav Raj SandGitHub 607fc25723 Bugfix: Fix copy in agent availability status (#466)
Copy update for agent status
2020-02-05 12:11:33 +05:45
83b0bb4062 Feature: As an end-user, I should be able to see the list of agents in the widget. (#461)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-05 11:27:22 +05:30
33e0bd434b Chore: Update the copy used in signup page (#464)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-02-03 20:12:37 +05:45
Sojan 002463aede Merge branch 'hotfix/1.1.2' into develop 2020-02-03 14:33:53 +05:45
Sojan f1613f4a0d Merge branch 'hotfix/1.1.2' 2020-02-03 14:33:43 +05:45
Sojan 1aebdc63d5 Fix coverage issue with codeclimate 2020-02-03 14:29:03 +05:45
Sojan 8d6f9cad05 Merge branch 'hotfix/1.1.1' into develop 2020-02-03 14:14:39 +05:45
Sojan 03a46d0cfe Merge branch 'hotfix/1.1.1' 2020-02-03 14:14:29 +05:45
Sojan 067411b5b6 Fix heroku deploy and docker image issues
#458
#459
2020-02-03 14:02:32 +05:45
348 changed files with 11846 additions and 3965 deletions
+7 -7
View File
@@ -12,8 +12,8 @@ defaults: &defaults
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: circleci/postgres:9.4
- image: circleci/redis:5.0.7-alpine
- image: circleci/postgres:alpine
- image: circleci/redis:alpine
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
@@ -95,24 +95,24 @@ jobs:
command: yarn run eslint
# Run rails tests
- run:
- 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
- persist_to_workspace:
root: tmp
paths:
paths:
- codeclimate.backend.json
- run:
- run:
name: Run frontend tests
command: |
yarn test:coverage
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info
- persist_to_workspace:
root: tmp
paths:
paths:
- codeclimate.frontend.json
# collect reports
@@ -126,4 +126,4 @@ jobs:
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 upload-coverage -i tmp/codeclimate.total.json
+16 -2
View File
@@ -1,4 +1,12 @@
SECRET_KEY_BASE=
# Force all access to the app over SSL, default is set to false
FORCE_SSL=
# This lets you control new sign ups on your chatwoot installation
# true : default option, allows sign ups
# false : disables all the end points related to sign ups
# api_only: disables the UI for signup, but you can create sign ups via the account apis
ENABLE_ACCOUNT_SIGNUP=
#redis config
REDIS_URL=redis://redis:6379
@@ -20,14 +28,16 @@ FB_APP_SECRET=
FB_APP_ID=
#twitter app
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#mail
MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025
SMTP_DOMAIN=chatwoot.com
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
# else set the value as "localhost"
SMTP_ADDRESS=mailhog
SMTP_USERNAME=
@@ -48,6 +58,10 @@ AWS_REGION=
#sentry
SENTRY_DSN=
#Log settings
LOG_LEVEL=
LOG_SIZE=
# Credentials to access sidekiq dashboard in production
SIDEKIQ_AUTH_USERNAME=
SIDEKIQ_AUTH_PASSWORD=
@@ -59,4 +73,4 @@ ENABLE_BILLING=
CHARGEBEE_API_KEY=
CHARGEBEE_SITE=
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
CHARGEBEE_WEBHOOK_PASSWORD=
+4 -2
View File
@@ -24,10 +24,12 @@ module.exports = {
'multiline': {
'max': 1,
'allowFirstLine': false
}
},
}],
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off'
"vue/no-v-html": 'off',
'import/extensions': ['off']
},
settings: {
'import/resolver': {
+7 -1
View File
@@ -37,6 +37,10 @@ public/packs*
*.swo
*.un~
.jest-cache
#VS Code files
.vscode
# ignore jetbrains IDE files
.idea
@@ -48,4 +52,6 @@ coverage
# ignore packages
node_modules
package-lock.json
package-lock.json
*.dump
+3 -3
View File
@@ -20,14 +20,12 @@ Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
- 'lib/redis/alfred.rb'
- 'app/controllers/api/v1/webhooks_controller.rb'
- 'app/services/twitter/send_reply_service.rb'
- 'spec/services/twitter/send_reply_service_spec.rb'
Metrics/BlockLength:
Exclude:
- spec/**/*
- '**/routes.rb'
- 'config/environments/*'
- db/schema.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -41,6 +39,8 @@ Style/ClassAndModuleChildren:
RSpec/NestedGroups:
Enabled: true
Max: 4
RSpec/MessageSpies:
Enabled: false
AllCops:
Exclude:
- db/*
+274
View File
@@ -1,7 +1,281 @@
# Default application configuration that all configurations inherit from.
scss_files: '**/*.scss'
plugin_directories: ['.scss-linters']
# List of gem names to load custom linters from (make sure they are already
# installed)
plugin_gems: []
# Default severity of all linters.
severity: warning
linters:
BangFormat:
enabled: true
space_before_bang: true
space_after_bang: false
BemDepth:
enabled: false
max_elements: 1
BorderZero:
enabled: true
convention: zero # or `none`
ChainedClasses:
enabled: false
ColorKeyword:
enabled: true
ColorVariable:
enabled: true
Comment:
enabled: true
style: silent
DebugStatement:
enabled: true
DeclarationOrder:
enabled: true
DisableLinterReason:
enabled: false
DuplicateProperty:
enabled: true
ElsePlacement:
enabled: true
style: same_line # or 'new_line'
EmptyLineBetweenBlocks:
enabled: true
ignore_single_line_blocks: true
EmptyRule:
enabled: true
ExtendDirective:
enabled: false
FinalNewline:
enabled: true
present: true
HexLength:
enabled: true
style: short # or 'long'
HexNotation:
enabled: true
style: lowercase # or 'uppercase'
HexValidation:
enabled: true
IdSelector:
enabled: true
ImportantRule:
enabled: true
ImportPath:
enabled: true
leading_underscore: false
filename_extension: false
Indentation:
enabled: true
allow_non_nested_indentation: false
character: space # or 'tab'
width: 2
LeadingZero:
enabled: false
MergeableSelector:
enabled: true
force_nesting: true
NameFormat:
enabled: true
allow_leading_underscore: true
convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern
NestingDepth:
enabled: true
max_depth: 6
ignore_parent_selectors: false
PlaceholderInExtend:
enabled: true
PrivateNamingConvention:
enabled: false
prefix: _
PropertyCount:
enabled: false
include_nested: false
max_properties: 10
PropertySortOrder:
enabled: true
ignore_unspecified: false
min_properties: 2
separate_groups: false
PropertySpelling:
enabled: true
extra_properties: []
disabled_properties: []
PropertyUnits:
enabled: true
global: [
'ch',
'em',
'ex',
'rem', # Font-relative lengths
'cm',
'in',
'mm',
'pc',
'pt',
'px',
'q', # Absolute lengths
'vh',
'vw',
'vmin',
'vmax', # Viewport-percentage lengths
'fr', # Grid fractional lengths
'deg',
'grad',
'rad',
'turn', # Angle
'ms',
's', # Duration
'Hz',
'kHz', # Frequency
'dpi',
'dpcm',
'dppx', # Resolution
'%',
] # Other
properties: {}
PseudoElement:
enabled: true
QualifyingElement:
enabled: true
allow_element_with_attribute: false
allow_element_with_class: false
allow_element_with_id: false
SelectorDepth:
enabled: true
max_depth: 5
SelectorFormat:
enabled: false
Shorthand:
enabled: true
allowed_shorthands: [1, 2, 3, 4]
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
SingleLinePerSelector:
enabled: true
SpaceAfterComma:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space'
SpaceAfterComment:
enabled: false
style: one_space # or 'no_space', or 'at_least_one_space'
allow_empty_comments: true
SpaceAfterPropertyColon:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
SpaceAfterPropertyName:
enabled: true
SpaceAfterVariableColon:
enabled: false
style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline'
SpaceAfterVariableName:
enabled: true
SpaceAroundOperator:
enabled: true
style: one_space # or 'at_least_one_space', or 'no_space'
SpaceBeforeBrace:
enabled: true
style: space # or 'new_line'
allow_single_line_padding: false
SpaceBetweenParens:
enabled: true
spaces: 0
StringQuotes:
enabled: true
style: single_quotes # or double_quotes
TrailingSemicolon:
enabled: true
TrailingWhitespace:
enabled: true
TrailingZero:
enabled: false
TransitionAll:
enabled: false
UnnecessaryMantissa:
enabled: false
UnnecessaryParentReference:
enabled: true
UrlFormat:
enabled: true
UrlQuotes:
enabled: true
VariableForProperty:
enabled: false
properties: []
VendorPrefix:
enabled: true
identifier_list: base
additional_identifiers: []
excluded_identifiers: []
ZeroUnit:
enabled: true
Compass::*:
enabled: false
exclude:
- 'app/javascript/widget/assets/scss/_reset.scss'
- 'app/javascript/widget/assets/scss/sdk.css'
+11 -2
View File
@@ -16,6 +16,7 @@ gem 'hashie'
gem 'jbuilder'
gem 'kaminari'
gem 'responders'
gem 'rest-client'
gem 'time_diff'
gem 'tzinfo-data'
gem 'valid_email2'
@@ -60,7 +61,6 @@ gem 'chargebee'
##--- gems for channels ---##
gem 'facebook-messenger'
gem 'telegram-bot-ruby'
gem 'twitter'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
@@ -72,19 +72,27 @@ gem 'haikunator'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
gem 'scout_apm'
gem 'sentry-raven'
##-- background job processing --##
gem 'sidekiq'
##-- used for single column multiple binary flags in notification settings/feature flagging --##
gem 'flag_shih_tzu'
group :development do
gem 'annotate'
gem 'bullet'
gem 'letter_opener'
gem 'web-console'
# used in swagger build
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
end
group :development, :test do
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
gem 'action-cable-testing'
gem 'bundle-audit', require: false
gem 'byebug', platform: :mri
@@ -98,9 +106,10 @@ group :development, :test do
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'scss_lint', require: false
gem 'seed_dump'
gem 'shoulda-matchers'
gem 'simplecov', require: false
gem 'simplecov', '0.17.1', require: false
gem 'spring'
gem 'spring-watcher-listen'
end
+97 -97
View File
@@ -1,65 +1,73 @@
GIT
remote: https://github.com/chatwoot/twitty
revision: a005f8f6740fc8d2d3500701e1ab4ab0f1416c26
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
specs:
twitty (0.1.0)
oauth
GIT
remote: https://github.com/tzmfreedom/json_refs
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
ref: e32deb0
specs:
json_refs (0.1.2)
hana
GEM
remote: https://rubygems.org/
specs:
action-cable-testing (0.6.0)
actioncable (>= 5.0)
actioncable (6.0.2.1)
actionpack (= 6.0.2.1)
actioncable (6.0.2.2)
actionpack (= 6.0.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
actionmailbox (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
mail (>= 2.7.1)
actionmailer (6.0.2.1)
actionpack (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.2.1)
actionview (= 6.0.2.1)
activesupport (= 6.0.2.1)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.2.1)
actionpack (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
actiontext (6.0.2.2)
actionpack (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
nokogiri (>= 1.8.5)
actionview (6.0.2.1)
activesupport (= 6.0.2.1)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.2.1)
activesupport (= 6.0.2.1)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
globalid (>= 0.3.6)
activemodel (6.0.2.1)
activesupport (= 6.0.2.1)
activerecord (6.0.2.1)
activemodel (= 6.0.2.1)
activesupport (= 6.0.2.1)
activestorage (6.0.2.1)
actionpack (= 6.0.2.1)
activejob (= 6.0.2.1)
activerecord (= 6.0.2.1)
activemodel (6.0.2.2)
activesupport (= 6.0.2.2)
activerecord (6.0.2.2)
activemodel (= 6.0.2.2)
activesupport (= 6.0.2.2)
activestorage (6.0.2.2)
actionpack (= 6.0.2.2)
activejob (= 6.0.2.2)
activerecord (= 6.0.2.2)
marcel (~> 0.3.1)
activesupport (6.0.2.1)
activesupport (6.0.2.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -73,9 +81,9 @@ GEM
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.2)
attr_extras (6.2.3)
aws-eventstream (1.0.3)
aws-partitions (1.268.0)
aws-partitions (1.269.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
@@ -109,7 +117,6 @@ GEM
msgpack (~> 1.0)
brakeman (4.7.2)
browser (3.0.3)
buftok (0.2.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
@@ -126,7 +133,7 @@ GEM
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.5)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crass (1.0.6)
declarative (0.0.10)
@@ -169,7 +176,8 @@ GEM
multipart-post (>= 1.2, < 3)
faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0)
ffi (1.12.1)
ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
@@ -202,17 +210,11 @@ GEM
os (>= 0.9, < 2.0)
signet (~> 0.12)
haikunator (1.1.0)
hashie (4.0.0)
http (3.3.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
hana (1.3.5)
hashie (4.1.0)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.2.0)
http_parser.rb (0.6.0)
httparty (0.17.3)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
@@ -228,18 +230,18 @@ GEM
json (2.3.0)
json_pure (2.2.0)
jwt (2.2.1)
kaminari (1.1.1)
kaminari (1.2.0)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1)
kaminari-activerecord (= 1.1.1)
kaminari-core (= 1.1.1)
kaminari-actionview (1.1.1)
kaminari-actionview (= 1.2.0)
kaminari-activerecord (= 1.2.0)
kaminari-core (= 1.2.0)
kaminari-actionview (1.2.0)
actionview
kaminari-core (= 1.1.1)
kaminari-activerecord (1.1.1)
kaminari-core (= 1.2.0)
kaminari-activerecord (1.2.0)
activerecord
kaminari-core (= 1.1.1)
kaminari-core (1.1.1)
kaminari-core (= 1.2.0)
kaminari-core (1.2.0)
koala (3.0.0)
addressable
faraday
@@ -259,13 +261,11 @@ GEM
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mimemagic (0.3.3)
mimemagic (0.3.4)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
@@ -275,11 +275,10 @@ GEM
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
naught (1.1.0)
netrc (0.11.0)
nightfury (1.0.1)
nio4r (2.5.2)
nokogiri (1.10.7)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
oauth (0.5.4)
orm_adapter (0.5.0)
@@ -294,11 +293,11 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
puma (4.3.1)
puma (4.3.3)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
rack (2.1.1)
rack (2.2.2)
rack-cache (1.11.0)
rack (>= 0.4)
rack-cors (1.1.1)
@@ -309,29 +308,29 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.2.1)
actioncable (= 6.0.2.1)
actionmailbox (= 6.0.2.1)
actionmailer (= 6.0.2.1)
actionpack (= 6.0.2.1)
actiontext (= 6.0.2.1)
actionview (= 6.0.2.1)
activejob (= 6.0.2.1)
activemodel (= 6.0.2.1)
activerecord (= 6.0.2.1)
activestorage (= 6.0.2.1)
activesupport (= 6.0.2.1)
rails (6.0.2.2)
actioncable (= 6.0.2.2)
actionmailbox (= 6.0.2.2)
actionmailer (= 6.0.2.2)
actionpack (= 6.0.2.2)
actiontext (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
activemodel (= 6.0.2.2)
activerecord (= 6.0.2.2)
activestorage (= 6.0.2.2)
activesupport (= 6.0.2.2)
bundler (>= 1.3.0)
railties (= 6.0.2.1)
railties (= 6.0.2.2)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.0.2.1)
actionpack (= 6.0.2.1)
activesupport (= 6.0.2.1)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@@ -387,12 +386,21 @@ GEM
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-performance (1.5.2)
rubocop (>= 0.71.0)
rubocop-rails (2.4.1)
rubocop-rails (2.4.2)
rack (>= 1.1)
rubocop (>= 0.72.0)
rubocop-rspec (1.37.1)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
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)
scout_apm (2.6.6)
parser
scss_lint (0.59.0)
sass (~> 3.5, >= 3.5.5)
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
@@ -410,7 +418,6 @@ GEM
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simple_oauth (0.3.1)
simplecov (0.17.1)
docile (~> 1.1)
json (>= 1.8, < 3)
@@ -427,7 +434,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
telegram-bot-ruby (0.11.0)
telegram-bot-ruby (0.12.0)
faraday
inflecto
virtus
@@ -436,17 +443,6 @@ GEM
time_diff (0.3.0)
activesupport
i18n
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (~> 0.0.11)
http (~> 3.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
simple_oauth (~> 0.3.0)
tzinfo (1.2.6)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
@@ -482,7 +478,7 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4)
wisper (2.0.0)
zeitwerk (2.2.2)
zeitwerk (2.3.0)
PLATFORMS
ruby
@@ -507,11 +503,13 @@ DEPENDENCIES
facebook-messenger
factory_bot_rails
faker
flag_shih_tzu
foreman
google-cloud-storage
haikunator
hashie
jbuilder
json_refs!
jwt
kaminari
koala
@@ -530,21 +528,23 @@ DEPENDENCIES
redis-namespace
redis-rack-cache
responders
rest-client
rspec-rails (~> 4.0.0.beta2)
rubocop
rubocop-performance
rubocop-rails
rubocop-rspec
scout_apm
scss_lint
seed_dump
sentry-raven
shoulda-matchers
sidekiq
simplecov
simplecov (= 0.17.1)
spring
spring-watcher-listen
telegram-bot-ruby
time_diff
twitter
twitty!
tzinfo-data
uglifier
+1 -7
View File
@@ -52,16 +52,10 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
## Docker
You can use our official Docker image from [https://hub.docker.com/r/chatwoot/chatwoot](https://hub.docker.com/r/chatwoot/chatwoot)
```bash
docker pull chatwoot/chatwoot
```
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using docker composer.
## Contributors ✨
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
+1
View File
@@ -0,0 +1 @@
module.exports = '';
+5
View File
@@ -5,6 +5,7 @@ class ContactMergeAction
ActiveRecord::Base.transaction do
validate_contacts
merge_conversations
merge_messages
merge_contact_inboxes
remove_mergee_contact
end
@@ -26,6 +27,10 @@ class ContactMergeAction
Conversation.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
def merge_messages
Message.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
def merge_contact_inboxes
ContactInbox.where(contact_id: @mergee_contact.id).update(contact_id: @base_contact.id)
end
+2 -2
View File
@@ -2,7 +2,7 @@ require 'open-uri'
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, fb_id will NOT be nil,
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
# based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages.
@@ -121,7 +121,7 @@ class Messages::MessageBuilder
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
fb_id: response.identifier
source_id: response.identifier
}
end
@@ -23,7 +23,7 @@ class Messages::Outgoing::NormalBuilder
content: @content,
private: @private,
user_id: @user.id,
fb_id: @fb_id
source_id: @fb_id
}
end
end
+4 -6
View File
@@ -1,16 +1,14 @@
class Api::BaseController < ApplicationController
respond_to :json
before_action :authenticate_user!
unless Rails.env.development?
rescue_from StandardError do |exception|
Raven.capture_exception(exception)
render json: { error: '500 error', message: exception.message }.to_json, status: 500
end
end
private
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
@@ -0,0 +1,36 @@
class Api::V1::Account::WebhooksController < Api::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy]
def index
@webhooks = current_account.webhooks
end
def create
@webhook = current_account.webhooks.new(webhook_params)
@webhook.save!
end
def update
@webhook.update!(webhook_params)
end
def destroy
@webhook.destroy
head :ok
end
private
def webhook_params
params.require(:webhook).permit(:inbox_id, :url)
end
def fetch_webhook
@webhook = current_account.webhooks.find(params[:id])
end
def check_authorization
authorize(Webhook)
end
end
@@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists,
@@ -30,4 +31,8 @@ class Api::V1::AccountsController < Api::BaseController
def account_params
params.permit(:account_name, :email)
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false'
end
end
+17 -22
View File
@@ -1,8 +1,7 @@
require 'rest-client'
require 'telegram/bot'
class Api::V1::CallbacksController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:register_facebook_page]
skip_before_action :authenticate_user!, only: [:register_facebook_page], raise: false
class Api::V1::CallbacksController < Api::BaseController
before_action :inbox, only: [:reauthorize_page]
def register_facebook_page
user_access_token = params[:user_access_token]
@@ -25,15 +24,13 @@ class Api::V1::CallbacksController < ApplicationController
# get params[:inbox_id], current_account, params[:omniauth_token]
def reauthorize_page
if @inbox&.first&.facebook?
if @inbox&.facebook?
fb_page_id = @inbox.channel.page_id
page_details = fb_object.get_connections('me', 'accounts')
(page_details || []).each do |page_detail|
if fb_page_id == page_detail['id'] # found the page which has to be reauthorised
update_fb_page(fb_page_id, page_detail['access_token'])
head :ok
end
if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] })
update_fb_page(fb_page_id, page_detail['access_token'])
return head :ok
end
end
@@ -46,18 +43,13 @@ class Api::V1::CallbacksController < ApplicationController
@inbox = current_account.inboxes.find_by(id: params[:inbox_id])
end
def update_fb_page
if fb_page(fb_page_id)
fb_page.update_attributes!(
user_access_token: @user_access_token, page_access_token: access_token
)
head :ok
else
head :unprocessable_entity
end
def update_fb_page(fb_page_id, access_token)
get_fb_page(fb_page_id)&.update!(
user_access_token: @user_access_token, page_access_token: access_token
)
end
def fb_page(fb_page_id)
def get_fb_page(fb_page_id)
current_account.facebook_pages.find_by(page_id: fb_page_id)
end
@@ -68,7 +60,7 @@ class Api::V1::CallbacksController < ApplicationController
def long_lived_token(omniauth_token)
koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET'])
long_lived_token = koala.exchange_access_token_info(omniauth_token)['access_token']
koala.exchange_access_token_info(omniauth_token)['access_token']
end
def mark_already_existing_facebook_pages(data)
@@ -81,7 +73,11 @@ class Api::V1::CallbacksController < ApplicationController
end
def set_avatar(facebook_channel, page_id)
avatar_resource = LocalResource.new(get_avatar_url(page_id))
uri = get_avatar_url(page_id)
return unless uri
avatar_resource = LocalResource.new(uri)
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end
@@ -98,7 +94,6 @@ class Api::V1::CallbacksController < ApplicationController
raise
end
pic_url = response.base_uri.to_s
Rails.logger.info(pic_url)
rescue StandardError => e
pic_url = nil
end
@@ -1,8 +1,18 @@
class Api::V1::Conversations::MessagesController < Api::BaseController
before_action :set_conversation, only: [:create]
before_action :set_conversation, only: [:index, :create]
def index
@messages = message_finder.perform
end
def create
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
@message = mb.perform
end
private
def message_finder
@message_finder ||= MessageFinder.new(@conversation, params)
end
end
@@ -1,11 +1,5 @@
class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: [:index, :get_messages]
# TODO: move this to public controller
skip_before_action :authenticate_user!, only: [:get_messages]
skip_before_action :set_current_user, only: [:get_messages]
skip_before_action :check_subscription, only: [:get_messages]
skip_around_action :handle_with_exception, only: [:get_messages]
before_action :set_conversation, except: [:index]
def index
result = conversation_finder.perform
@@ -13,9 +7,7 @@ class Api::V1::ConversationsController < Api::BaseController
@conversations_count = result[:count]
end
def show
@messages = messages_finder.perform
end
def show; end
def toggle_status
@status = @conversation.toggle_status
@@ -27,11 +19,6 @@ class Api::V1::ConversationsController < Api::BaseController
head :ok
end
def get_messages
@conversation = Conversation.find(params[:id])
@messages = messages_finder.perform
end
private
def parsed_last_seen_at
@@ -45,8 +32,4 @@ class Api::V1::ConversationsController < Api::BaseController
def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
def messages_finder
@message_finder ||= MessageFinder.new(@conversation, params)
end
end
@@ -4,17 +4,11 @@ class Api::V1::InboxMembersController < Api::BaseController
def create
# update also done via same action
if @inbox
begin
update_agents_list
head :ok
rescue StandardError => e
Rails.logger.debug "Rescued: #{e.inspect}"
render_could_not_create_error('Could not add agents to inbox')
end
else
render_not_found_error('Agents or inbox not found')
end
update_agents_list
head :ok
rescue StandardError => e
Rails.logger.debug "Rescued: #{e.inspect}"
render_could_not_create_error('Could not add agents to inbox')
end
def show
+9 -1
View File
@@ -1,6 +1,6 @@
class Api::V1::InboxesController < Api::BaseController
before_action :check_authorization
before_action :fetch_inbox, only: [:destroy]
before_action :fetch_inbox, only: [:destroy, :update]
def index
@inboxes = policy_scope(current_account.inboxes)
@@ -11,6 +11,10 @@ class Api::V1::InboxesController < Api::BaseController
head :ok
end
def update
@inbox.update(inbox_update_params)
end
private
def fetch_inbox
@@ -20,4 +24,8 @@ class Api::V1::InboxesController < Api::BaseController
def check_authorization
authorize(Inbox)
end
def inbox_update_params
params.require(:inbox).permit(:enable_auto_assignment)
end
end
@@ -1,5 +1,5 @@
class Api::V1::ProfilesController < Api::BaseController
before_action :fetch_user
before_action :set_user
def show
render json: @user
@@ -7,12 +7,11 @@ class Api::V1::ProfilesController < Api::BaseController
def update
@user.update!(profile_params)
render json: @user
end
private
def fetch_user
def set_user
@user = current_user
end
@@ -1,6 +1,8 @@
class Api::V1::SubscriptionsController < ApplicationController
class Api::V1::SubscriptionsController < Api::BaseController
skip_before_action :check_subscription
before_action :check_billing_enabled
def index
render json: current_account.subscription_data
end
@@ -0,0 +1,29 @@
class Api::V1::User::NotificationSettingsController < Api::BaseController
before_action :set_user, :load_notification_setting
def show; end
def update
update_flags
@notification_setting.save!
render action: 'show'
end
private
def set_user
@user = current_user
end
def load_notification_setting
@notification_setting = @user.notification_settings.find_by(account_id: current_account.id)
end
def notification_setting_params
params.require(:notification_settings).permit(selected_email_flags: [])
end
def update_flags
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
end
end
@@ -4,6 +4,7 @@ class Api::V1::WebhooksController < ApplicationController
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
@@ -13,7 +14,7 @@ class Api::V1::WebhooksController < ApplicationController
end
def twitter_crc
render json: { response_token: "sha256=#{$twitter.generate_crc(params[:crc_token])}" }
render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
end
def twitter_events
@@ -26,6 +27,12 @@ class Api::V1::WebhooksController < ApplicationController
private
def twitter_client
Twitty::Facade.new do |config|
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
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']
@@ -31,9 +31,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def message_params
{
account_id: conversation.account_id,
contact_id: @contact.id,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
message_type: :incoming,
content: permitted_params[:message][:content]
message_type: :incoming
}
end
+1 -1
View File
@@ -67,7 +67,7 @@ class ApplicationController < ActionController::Base
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 unless ENV['BILLING_ENABLED']
return if !ENV['BILLING_ENABLED'] || !current_user
if current_subscription.trial? && current_subscription.expiry < Date.current
render json: { error: 'Trial Expired' }, status: :trial_expired
@@ -2,4 +2,8 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
def render_create_success
render 'devise/auth.json'
end
end
+1 -5
View File
@@ -1,13 +1,9 @@
require 'rest-client'
require 'telegram/bot'
class HomeController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:telegram]
skip_before_action :authenticate_user!, only: [:telegram], raise: false
skip_before_action :set_current_user
skip_before_action :check_subscription
def index; end
def status
head :ok
end
end
+18
View File
@@ -0,0 +1,18 @@
class SwaggerController < ApplicationController
def respond
if Rails.env.development? || Rails.env.test?
render inline: File.read(Rails.root.join('swagger', derived_path))
else
head 404
end
end
private
def derived_path
params[:path] ||= 'index.html'
path = params[:path]
path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s)
path
end
end
@@ -0,0 +1,30 @@
class Twitter::AuthorizationsController < Twitter::BaseController
def create
@response = twitter_client.request_oauth_token(url: twitter_callback_url)
if @response.status == '200'
::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def oauth_token
parsed_body['oauth_token']
end
def user
@user ||= User.find_by(id: params[:user_id])
end
def account
@account ||= user.account
end
def oauth_authorize_endpoint(oauth_token)
"#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}"
end
end
@@ -0,0 +1,24 @@
class Twitter::BaseController < ApplicationController
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def host
ENV.fetch('FRONTEND_URL', '')
end
def twitter_client
Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.base_url = twitter_api_base_url
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
end
end
def twitter_api_base_url
'https://api.twitter.com'
end
end
@@ -0,0 +1,53 @@
class Twitter::CallbacksController < Twitter::BaseController
def show
return redirect_to app_new_twitter_inbox_url if permitted_params[:denied]
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
)
if @response.status == '200'
inbox = build_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def account_id
::Redis::Alfred.get(permitted_params[:oauth_token])
end
def account
@account ||= Account.find_by!(id: account_id)
end
def build_inbox
ActiveRecord::Base.transaction do
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'],
name: parsed_body['screen_name']
)
account.inboxes.create(
name: parsed_body['screen_name'],
channel: twitter_profile
)
rescue StandardError => e
Rails.logger e
end
end
def permitted_params
params.permit(:oauth_token, :oauth_verifier, :denied)
end
end
+1 -3
View File
@@ -4,9 +4,7 @@ class WidgetsController < ActionController::Base
before_action :set_contact
before_action :build_contact
def index
render
end
def index; end
private
+1 -1
View File
@@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher
end
def listeners
listeners = [ReportingListener.instance]
listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
listeners
end
+5 -12
View File
@@ -1,23 +1,18 @@
class ConversationFinder
attr_reader :current_user, :current_account, :params
ASSIGNEE_TYPES = { me: 0, unassigned: 1, all: 2 }.freeze
ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert
ASSIGNEE_TYPES_BY_ID.default = :me
DEFAULT_STATUS = 'open'.freeze
# assumptions
# inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'me'
# assignee_type if not given, take 'all'
# conversation_status if not given, take 'open'
# response of this class will be of type
# {conversations: [array of conversations], count: {open: count, resolved: count}}
# params
# assignee_type_id, inbox_id, :status
# assignee_type, inbox_id, :status
def initialize(current_user, params)
@current_user = current_user
@@ -62,7 +57,7 @@ class ConversationFinder
end
def set_assignee_type
@assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]]
@assignee_type = params[:assignee_type]
end
def find_all_conversations
@@ -72,12 +67,10 @@ class ConversationFinder
end
def filter_by_assignee_type
if @assignee_type_id == ASSIGNEE_TYPES[:me]
if @assignee_type == 'me'
@conversations = @conversations.assigned_to(current_user)
elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned]
elsif @assignee_type == 'unassigned'
@conversations = @conversations.unassigned
elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
@conversations
end
@conversations
end
-2
View File
@@ -1,2 +0,0 @@
module Api::V1::WebhooksHelper
end
+1 -2
View File
@@ -18,8 +18,7 @@ export default {
},
mounted() {
this.$store.dispatch('set_user');
this.$store.dispatch('validityCheck');
this.$store.dispatch('setUser');
},
};
</script>
+21 -41
View File
@@ -1,29 +1,10 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint-env browser */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import moment from 'moment';
import Cookies from 'js-cookie';
import endPoints from './endPoints';
import { frontendURL } from '../helper/URLHelper';
const setAuthCredentials = response => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, {
expires: expiryDate.diff(moment(), 'days'),
});
Cookies.set('user', response.data.data, {
expires: expiryDate.diff(moment(), 'days'),
});
};
const clearCookiesOnLogout = () => {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = frontendURL('login');
};
import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api';
export default {
login(creds) {
@@ -60,20 +41,7 @@ export default {
},
validityCheck() {
const urlData = endPoints('validityCheck');
const fetchPromise = new Promise((resolve, reject) => {
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
if (error.response.status === 401) {
clearCookiesOnLogout();
}
reject(error);
});
});
return fetchPromise;
return axios.get(urlData.url);
},
logout() {
const urlData = endPoints('logout');
@@ -136,13 +104,7 @@ export default {
password,
})
.then(response => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, {
expires: expiryDate.diff(moment(), 'days'),
});
Cookies.set('user', response.data.data, {
expires: expiryDate.diff(moment(), 'days'),
});
setAuthCredentials(response);
resolve(response);
})
.catch(error => {
@@ -155,4 +117,22 @@ export default {
const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email });
},
profileUpdate({ name, email, password, password_confirmation, avatar }) {
const formData = new FormData();
if (name) {
formData.append('profile[name]', name);
}
if (email) {
formData.append('profile[email]', email);
}
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);
},
};
@@ -10,8 +10,8 @@ class ConversationApi extends ApiClient {
return axios.get(`${this.url}/${conversationID}/labels`);
}
createLabels(conversationID) {
return axios.get(`${this.url}/${conversationID}/labels`);
updateLabels(conversationID, labels) {
return axios.post(`${this.url}/${conversationID}/labels`, { labels });
}
}
@@ -10,6 +10,9 @@ const endPoints = {
validityCheck: {
url: '/auth/validate_token',
},
profileUpdate: {
url: '/api/v1/profile',
},
logout: {
url: 'auth/sign_out',
},
@@ -6,12 +6,13 @@ class ConversationApi extends ApiClient {
super('conversations');
}
get({ inboxId, status, assigneeType }) {
get({ inboxId, status, assigneeType, page }) {
return axios.get(this.url, {
params: {
inbox_id: inboxId,
status,
assignee_type_id: assigneeType,
assignee_type: assigneeType,
page,
},
});
}
@@ -15,7 +15,7 @@ class MessageApi extends ApiClient {
}
getPreviousMessages({ conversationId, before }) {
return axios.get(`${this.url}/${conversationId}`, {
return axios.get(`${this.url}/${conversationId}/messages`, {
params: { before },
});
}
@@ -10,6 +10,6 @@ describe('#ConversationApi', () => {
expect(conversations).toHaveProperty('update');
expect(conversations).toHaveProperty('delete');
expect(conversations).toHaveProperty('getLabels');
expect(conversations).toHaveProperty('createLabels');
expect(conversations).toHaveProperty('updateLabels');
});
});
@@ -0,0 +1,13 @@
import userNotificationSettings from '../userNotificationSettings';
import ApiClient from '../ApiClient';
describe('#AgentAPI', () => {
it('creates correct instance', () => {
expect(userNotificationSettings).toBeInstanceOf(ApiClient);
expect(userNotificationSettings).toHaveProperty('get');
expect(userNotificationSettings).toHaveProperty('show');
expect(userNotificationSettings).toHaveProperty('create');
expect(userNotificationSettings).toHaveProperty('update');
expect(userNotificationSettings).toHaveProperty('delete');
});
});
@@ -0,0 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class UserNotificationSettings extends ApiClient {
constructor() {
super('user/notification_settings');
}
update(params) {
return axios.patch(`${this.url}`, params);
}
}
export default new UserNotificationSettings();
+9
View File
@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class WebHooks extends ApiClient {
constructor() {
super('account/webhooks');
}
}
export default new WebHooks();
Binary file not shown.
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#4D4D4D;" d="M188.287,512c-41.473,0-75.213-33.74-75.213-75.213V246.75c0-4.142,3.358-7.5,7.5-7.5
s7.5,3.358,7.5,7.5v190.037c0,33.202,27.011,60.213,60.213,60.213c16.082,0,31.204-6.266,42.582-17.644
c11.37-11.37,17.631-26.488,17.631-42.569V75.213C248.5,33.74,282.24,0,323.713,0c20.088,0,38.978,7.826,53.189,22.037
c14.203,14.202,22.024,33.087,22.024,53.176V256c0,4.142-3.358,7.5-7.5,7.5s-7.5-3.358-7.5-7.5V75.213
c0-16.082-6.261-31.2-17.63-42.569C354.918,21.266,339.794,15,323.713,15C290.511,15,263.5,42.011,263.5,75.213v361.574
c0,20.088-7.822,38.973-22.024,53.176C227.265,504.174,208.376,512,188.287,512z"/>
<g>
<rect x="113.07" y="246.75" style="fill:#3B3B3B;" width="15" height="26.875"/>
<rect x="383.93" y="235.31" style="fill:#3B3B3B;" width="15" height="26.875"/>
</g>
<rect x="361.9" y="385" style="fill:#CCCCCC;" width="57.983" height="39.944"/>
<rect x="361.9" y="385" style="fill:#ADADAD;" width="57.983" height="22.19"/>
<path style="fill:#A6E2E3;" d="M432.802,298.678v86.977c0,3.616-2.932,6.548-6.548,6.548h-70.721c-3.617,0-6.548-2.932-6.548-6.548
v-87.746c0-23.439,19.239-42.39,42.803-41.899C414.709,256.486,432.802,275.751,432.802,298.678z"/>
<rect x="92.11" y="87.06" style="fill:#CCCCCC;" width="57.983" height="36.28"/>
<rect x="92.11" y="105.43" style="fill:#ADADAD;" width="57.983" height="17.907"/>
<path style="fill:#FFA638;" d="M163.015,126.345v86.977c0,22.927-18.093,42.191-41.015,42.668
c-23.564,0.49-42.803-18.461-42.803-41.899v-87.746c0-3.616,2.932-6.548,6.548-6.548l0,0h70.721l0,0
C160.083,119.797,163.015,122.729,163.015,126.345z"/>
<path style="fill:#7CCBCC;" d="M391.787,256.009c-5.066-0.105-9.93,0.693-14.447,2.236c0.396-0.081,0.781-0.166,1.142-0.257
c2.982-0.755,5.201-0.896,7.513-0.85c18.954,0.395,34.375,16.494,34.375,35.888v86.981c0,3.614-2.93,6.544-6.544,6.544H355.53
c-3.614,0-6.544-2.93-6.544-6.544l0,0v5.648c0,3.616,2.932,6.548,6.548,6.548h70.721c3.617,0,6.548-2.932,6.548-6.548v-86.977
C432.802,275.751,414.709,256.486,391.787,256.009z"/>
<path style="fill:#EB7100;" d="M79.527,217.153l-0.23-0.322c0.081,1.261,0.209,2.509,0.399,3.737
C79.586,219.444,79.527,218.305,79.527,217.153z"/>
<path style="fill:#ED8300;" d="M156.467,119.797h-11.188c2.613,0.858,4.502,3.314,4.502,6.215v90.372
c0,19.395-15.42,35.494-34.375,35.888c-0.251,0.005-0.503,0.008-0.753,0.008c-9.651,0-18.405-3.914-24.76-10.236
c7.856,8.766,19.342,14.212,32.106,13.947c22.922-0.477,41.015-19.741,41.015-42.668v-86.977
C163.015,122.728,160.083,119.797,156.467,119.797z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -28,7 +28,7 @@
code {
border: 0;
font-family: 'Monaco';
font-family: 'Monaco', Verdana;
font-size: $font-size-mini;
&.hljs {
@@ -41,29 +41,34 @@
// 36. Tooltip
// 37. Top Bar
@import "~foundation-sites/scss/util/util";
@import '~foundation-sites/scss/util/util';
// 1. Global
// ---------
$global-font-size: 10px;
$global-width: 100%;
$global-lineheight: 1.5;
$foundation-palette: (
primary: $color-woot,
$foundation-palette: (primary: $color-woot,
secondary: #777,
success: #13ce66,
warning: #ffc82c,
alert: #ff4949
);
alert: #ff4949);
$light-gray: #c0ccda;
$medium-gray: #8492a6;
$dark-gray: $color-gray;
$black: #000000;
$black: #000;
$white: #fff;
$body-background: $white;
$body-font-color: $color-body;
$body-font-family: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
$body-font-family: 'Inter',
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif;
$body-antialiased: true;
$global-margin: $space-one;
$global-padding: $space-one;
@@ -79,13 +84,11 @@ $print-transparent-backgrounds: true;
// 2. Breakpoints
// --------------
$breakpoints: (
small: 0,
$breakpoints: (small: 0,
medium: 640px,
large: 1024px,
xlarge: 1200px,
xxlarge: 1440px
);
xxlarge: 1440px);
$print-breakpoint: large;
$breakpoint-classes: (small medium large);
@@ -94,10 +97,8 @@ $breakpoint-classes: (small medium large);
$grid-row-width: $global-width;
$grid-column-count: 12;
$grid-column-gutter: (
small: $zero,
medium: $zero
);
$grid-column-gutter: (small: $zero,
medium: $zero);
$grid-column-align-edge: true;
$block-grid-max: 8;
@@ -105,54 +106,24 @@ $block-grid-max: 8;
// ------------------
$header-font-family: $body-font-family;
$header-font-weight: $global-weight-normal;
$header-font-weight: $font-weight-medium;
$header-font-style: normal;
$font-family-monospace: $body-font-family;
$header-color: $color-heading;
$header-lineheight: 1.4;
$header-margin-bottom: 0.5rem;
$header-styles: (
small: (
"h1": (
"font-size": 24
),
"h2": (
"font-size": 20
),
"h3": (
"font-size": 19
),
"h4": (
"font-size": 18
),
"h5": (
"font-size": 17
),
"h6": (
"font-size": 16
)
),
medium: (
"h1": (
"font-size": 48
),
"h2": (
"font-size": 40
),
"h3": (
"font-size": 31
),
"h4": (
"font-size": 25
),
"h5": (
"font-size": 20
),
"h6": (
"font-size": 16
)
)
);
$header-styles: (small: ("h1": ("font-size": 24),
"h2": ("font-size": 20),
"h3": ("font-size": 19),
"h4": ("font-size": 18),
"h5": ("font-size": 17),
"h6": ("font-size": 16)),
medium: ("h1": ("font-size": 48),
"h2": ("font-size": 40),
"h3": ("font-size": 31),
"h4": ("font-size": 25),
"h5": ("font-size": 20),
"h6": ("font-size": 16)));
$header-text-rendering: optimizeLegibility;
$small-font-size: 80%;
$header-small-font-color: $medium-gray;
@@ -186,7 +157,7 @@ $blockquote-padding: rem-calc(9 20 0 19);
$blockquote-border: 1px solid $medium-gray;
$cite-font-size: rem-calc(13);
$cite-color: $dark-gray;
$cite-pseudo-content: "\2014 \0020";
$cite-pseudo-content: '\2014 \0020';
$keystroke-font: $font-family-monospace;
$keystroke-color: $black;
$keystroke-background: $light-gray;
@@ -272,24 +243,23 @@ $button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white;
$button-color-alt: $white;
$button-radius: $global-radius;
$button-sizes: (
tiny: $font-size-micro,
$button-sizes: (tiny: $font-size-micro,
small: $font-size-mini,
default: $font-size-default,
large: $font-size-large
);
large: $font-size-large);
$button-palette: $foundation-palette;
$button-opacity-disabled: 0.25;
$button-background-hover-lightness: -20%;
$button-hollow-hover-lightness: -50%;
$button-transition: background-color 0.25s ease-out, color 0.25s ease-out;
$button-transition: background-color 0.25s ease-out,
color 0.25s ease-out;
// 12. Button Group
// ----------------
$buttongroup-margin: 1rem;
$buttongroup-spacing: 1px;
$buttongroup-child-selector: ".button";
$buttongroup-child-selector: '.button';
$buttongroup-expand-max: 6;
$buttongroup-radius-on-each: true;
@@ -322,18 +292,12 @@ $card-margin: $global-margin;
// ----------------
$closebutton-position: right top;
$closebutton-offset-horizontal: (
small: 0.66rem,
medium: 1rem
);
$closebutton-offset-vertical: (
small: 0.33em,
medium: 0.5rem
);
$closebutton-size: (
small: 1.5em,
medium: 2em
);
$closebutton-offset-horizontal: (small: 0.66rem,
medium: 1rem);
$closebutton-offset-vertical: (small: 0.33em,
medium: 0.5rem);
$closebutton-size: (small: 1.5em,
medium: 2em);
$closebutton-lineheight: 1;
$closebutton-color: $dark-gray;
$closebutton-color-hover: $black;
@@ -356,11 +320,9 @@ $dropdown-border: 1px solid $medium-gray;
$dropdown-font-size: 1rem;
$dropdown-width: 300px;
$dropdown-radius: $global-radius;
$dropdown-sizes: (
tiny: 100px,
$dropdown-sizes: (tiny: 100px,
small: 200px,
large: 400px
);
large: 400px);
// 18. Dropdown Menu
// -----------------
@@ -455,12 +417,10 @@ $meter-fill-bad: $alert-color;
// 24. Off-canvas
// --------------
$offcanvas-sizes: (
small: 23rem,
$offcanvas-sizes: (small: 23rem,
medium: 23rem,
);
$offcanvas-vertical-sizes: (
small: 23rem,
$offcanvas-vertical-sizes: (small: 23rem,
medium: 23rem,
);
$offcanvas-background: $light-gray;
@@ -472,7 +432,7 @@ $offcanvas-transition-length: 0.5s;
$offcanvas-transition-timing: ease;
$offcanvas-fixed-reveal: true;
$offcanvas-exit-background: rgba($white, 0.25);
$maincontent-class: "off-canvas-content";
$maincontent-class: 'off-canvas-content';
// 25. Orbit
// ---------
@@ -520,10 +480,8 @@ $progress-radius: $global-radius;
// --------------------
$responsive-embed-margin-bottom: rem-calc(16);
$responsive-embed-ratios: (
default: 4 by 3,
widescreen: 16 by 9
);
$responsive-embed-ratios: (default: 4 by 3,
widescreen: 16 by 9);
// 29. Reveal
// ----------
@@ -576,10 +534,8 @@ $table-border: 1px solid transparent;
$table-padding: rem-calc(8 10 10);
$table-hover-scale: 2%;
$table-row-hover: darken($table-background, $table-hover-scale);
$table-row-stripe-hover: darken(
$table-background,
$table-color-scale + $table-hover-scale
);
$table-row-stripe-hover: darken($table-background,
$table-color-scale + $table-hover-scale);
$table-is-striped: false;
$table-striped-background: smart-scale($table-background, $table-color-scale);
$table-stripe: even;
@@ -35,15 +35,17 @@ body {
flex-direction: column;
@include margin($zero);
@include padding($space-normal);
overflow-y: scroll;
overflow-y: auto;
}
.content-box {
overflow: scroll;
overflow: auto;
@include padding($space-normal);
}
.back-button {
@include flex;
align-items: center;
color: $color-woot;
font-size: $font-size-default;
font-weight: $font-weight-normal;
@@ -1,3 +1,8 @@
@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
@mixin border-nil() {
border-color: transparent;
@@ -77,8 +82,8 @@
&:active,
&:hover,
&:focus {
box-shadow: none;
border-color: transparent;
box-shadow: none;
}
}
@@ -117,7 +122,6 @@
// full height
@mixin full-height() {
height: 100%;
// COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes
}
@mixin round-corner() {
@@ -125,21 +129,20 @@
}
@mixin scroll-on-hover() {
transition: all .4s $ease-in-out-cubic;
overflow: hidden;
&:hover {
overflow-y: scroll;
overflow-y: auto;
}
}
@mixin horizontal-scroll() {
overflow-y: scroll;
overflow-y: auto;
}
@mixin elegent-shadow() {
box-shadow: 0 10px 25px 0 rgba(49,49,93,0.15);
box-shadow: 0 10px 25px 0 $elegant-shadow-color;
}
@mixin elegant-card() {
@@ -154,20 +157,20 @@
}
}
&:before {
content: '';
&::before {
animation: spinner .9s linear infinite;
border: 2px solid $spinner-before-border-color;
border-radius: 50%;
border-top-color: lighten($color-woot, 10%);
box-sizing: border-box;
content: '';
height: $space-medium;
left: 50%;
margin-left: -$space-one;
margin-top: -$space-one;
position: absolute;
top: 50%;
left: 50%;
width: $space-medium;
height: $space-medium;
margin-top: -$space-one;
margin-left: -$space-one;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.7);
border-top-color: lighten($color-woot, 10%);
animation: spinner .9s linear infinite;
}
}
@@ -181,41 +184,41 @@
// .element{
// @include arrow(top, #000, 50px);
// }
@mixin arrow($direction, $color, $size){
display: block;
height: 0;
width: 0;
content: '';
@mixin arrow($direction, $color, $size) {
display: block;
height: 0;
width: 0;
content: '';
@if $direction == 'top' {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-bottom: $size solid $color;
} @else if $direction == 'right' {
border-top: $size solid transparent;
border-bottom: $size solid transparent;
border-left: $size solid $color;
} @else if $direction == 'bottom' {
border-top: $size solid $color;
border-right: $size solid transparent;
border-left: $size solid transparent;
} @else if $direction == 'left' {
border-top: $size solid transparent;
border-right: $size solid $color;
border-bottom: $size solid transparent;
} @else if $direction == 'top-left' {
border-top: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'top-right' {
border-top: $size solid $color;
border-left: $size solid transparent;
} @else if $direction == 'bottom-left' {
border-bottom: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'bottom-right' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
}
@if $direction == 'top' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
border-right: $size solid transparent;
} @else if $direction == 'right' {
border-bottom: $size solid transparent;
border-left: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'bottom' {
border-left: $size solid transparent;
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'left' {
border-bottom: $size solid transparent;
border-right: $size solid $color;
border-top: $size solid transparent;
} @else if $direction == 'top-left' {
border-right: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'top-right' {
border-left: $size solid transparent;
border-top: $size solid $color;
} @else if $direction == 'bottom-left' {
border-bottom: $size solid $color;
border-right: $size solid transparent;
} @else if $direction == 'bottom-right' {
border-bottom: $size solid $color;
border-left: $size solid transparent;
}
}
@mixin text-ellipsis {
@@ -12,7 +12,7 @@ $font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
// spaces
$zero: 0rem;
$zero: 0;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
@@ -42,16 +42,27 @@ $woot-logo-padding: $space-large $space-two;
// Colors
$color-woot: #1f93ff;
$color-gray: #6E6F73;
$color-light-gray: #999A9B;
$color-border: #E0E6ED;
$color-gray: #6e6f73;
$color-light-gray: #999a9b;
$color-border: #e0e6ed;
$color-border-light: #f0f4f5;
$color-background: #EFF2F7;
$color-background-light: #F9FAFC;
$color-white: #FFF;
$color-body: #3C4858;
$color-heading: #1F2D3D;
$color-extra-light-blue: #F5F7F9;
$color-background: #eff2f7;
$color-background-light: #f9fafc;
$color-white: #fff;
$color-body: #3c4858;
$color-heading: #1f2d3d;
$color-extra-light-blue: #f5f7f9;
$primary-color: $color-woot;
$secondary-color: #ff5216;
$success-color: #13ce66;
$warning-color: #ffc82c;
$alert-color: #ff4949;
// Color-palettes
$color-primary-light: #c7e3ff;
$color-primary-dark: darken($color-woot, 20%);
// Thumbnail
$thumbnail-radius: 4rem;
@@ -81,3 +92,6 @@ $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !d
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions
$transition-ease-in: all 0.250s ease-in;
@@ -25,6 +25,7 @@
@import 'views/settings/inbox';
@import 'views/settings/channel';
@import 'views/settings/integrations';
@import 'views/signup';
@import 'plugins/multiselect';
@@ -1,32 +1,24 @@
@mixin label-multiselect-hover {
&::after {
color: $color-primary-dark;
}
&:hover {
background: $color-background;
&::after {
color: $color-woot;
}
}
}
.multiselect {
margin-bottom: $space-normal;
min-height: 38px;
> .multiselect__tags {
@include margin(0);
border: 1px solid $color-border;
min-height: 44px;
padding-top: $zero;
.multiselect__placeholder {
padding-top: $space-small;
}
.multiselect__tag {
margin-top: $space-one;
}
.multiselect__input {
@include ghost-input;
@include padding($zero);
margin-bottom: $zero;
}
.multiselect__single {
@include padding($space-one);
margin-bottom: 0;
&.multiselect--active {
>.multiselect__tags {
border-color: $color-woot;
}
}
@@ -41,4 +33,93 @@
top: 60%;
}
}
.multiselect__content .multiselect__option {
font-size: $font-size-small;
font-weight: $font-weight-normal;
&.multiselect__option--highlight {
font-weight: $font-weight-medium;
}
}
}
.multiselect>.multiselect__tags {
@include margin(0);
border: 1px solid $color-border;
min-height: 44px;
padding-top: $zero;
.multiselect__tags-wrap {
display: inline-block;
line-height: 1;
margin-top: $space-smaller;
}
.multiselect__placeholder {
color: $color-gray;
font-weight: $font-weight-normal;
padding-top: $space-small;
}
.multiselect__tag {
$vertical-space: $space-smaller + $space-micro;
background: $color-background;
color: $color-heading;
margin-top: $space-smaller;
padding: $vertical-space $space-medium $vertical-space $space-one;
}
.multiselect__tag-icon {
@include label-multiselect-hover;
line-height: $space-medium + $space-micro;
}
.multiselect__input {
@include ghost-input;
@include padding($zero);
font-size: $font-size-small;
margin-bottom: $zero;
}
.multiselect__single {
@include padding($space-one);
margin-bottom: 0;
}
}
.sidebar-labels-wrap {
&.has-edited,
&:hover {
.multiselect {
cursor: pointer;
}
.multiselect>.multiselect__tags {
border-color: $color-border;
}
.multiselect>.multiselect__select {
visibility: visible;
}
}
.multiselect {
margin-top: $space-small;
>.multiselect__select {
visibility: hidden;
}
>.multiselect__tags {
border-color: transparent;
}
&.multiselect--active>.multiselect__tags {
border-color: $color-woot;
}
}
}
@@ -212,7 +212,7 @@
.code {
max-height: $space-mega;
overflow: scroll;
overflow: auto;
white-space: nowrap;
@include padding($space-one);
background: $color-background;
@@ -0,0 +1,37 @@
.integrations-wrap {
.integration {
background: $color-white;
border: 2px solid $color-border;
border-radius: $space-slab;
padding: $space-normal;
.integration--image {
display: flex;
margin-right: $space-normal;
width: 8rem;
img {
max-width: 8rem;
padding: $space-small;
}
}
.integration--title {
font-size: $font-size-large;
}
.integration--description {
padding-right: $space-medium;
}
.button-wrap {
@include flex;
@include flex-align(center, middle);
margin-bottom: 0;
}
}
}
.help-wrap {
padding-left: $space-large;
}
@@ -24,9 +24,18 @@
}
}
> .icon {
>.icon {
font-size: $font-size-default;
}
&.tiny {
font-size: $font-size-mini;
padding: $space-small $space-slab;
}
&.round {
border-radius: $space-larger;
}
}
.button--fixed-right-top {
@@ -5,6 +5,7 @@
@include flex;
@include flex-align($x: justify, $y: middle);
@include border-normal-bottom;
// Resolve Button
.button {
@include margin(0);
@@ -44,6 +45,7 @@
.user--name {
@include margin(0);
font-size: $font-size-medium;
text-transform: capitalize;
}
.user--profile__meta {
@@ -64,7 +66,7 @@
}
.button.resolve--button {
> .icon {
>.icon {
padding-right: $space-small;
font-size: $font-size-default;
}
@@ -2,8 +2,8 @@
@include flex;
@include flex-shrink;
@include padding($space-normal $zero $zero $space-normal);
position: relative;
cursor: pointer;
position: relative;
&.active {
background: $color-background;
@@ -18,63 +18,63 @@
.conversation--user {
font-size: $font-size-small;
margin-bottom: $zero;
text-transform: capitalize;
.label {
position: relative;
top: $space-micro;
left: $space-micro;
max-width: $space-jumbo;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
top: $space-micro;
white-space: nowrap;
}
}
.conversation--message {
height: $space-medium;
margin: $zero;
font-size: $font-size-small;
line-height: $space-medium;
font-weight: $font-weight-light;
text-overflow: ellipsis;
overflow: hidden;
color: $color-body;
width: 27rem;
white-space: nowrap;
font-size: $font-size-small;
font-weight: $font-weight-normal;
height: $space-medium;
line-height: $space-medium;
margin: $zero;
max-width: 96%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 27rem;
}
.conversation--meta {
@include flex;
display: block;
flex-direction: column;
position: absolute;
right: $space-normal;
top: $space-normal;
@include flex;
flex-direction: column;
.unread {
$unread-size: $space-two - $space-micro;
display: none;
height: $unread-size;
min-width: $unread-size;
background: darken($success-color, 3%);
text-align: center;
padding: 0 $space-smaller;
line-height: $unread-size;
color: $color-white;
font-weight: $font-weight-medium;
font-size: $font-size-mini;
margin-left: auto;
@include round-corner;
background: darken($success-color, 3%);
color: $color-white;
display: none;
font-size: $font-size-mini;
font-weight: $font-weight-medium;
height: $unread-size;
line-height: $unread-size;
margin-left: auto;
margin-top: $space-smaller;
min-width: $unread-size;
padding: 0 $space-smaller;
text-align: center;
}
.timestamp {
font-size: $font-size-mini;
color: $dark-gray;
line-height: $space-normal;
font-weight: $font-weight-normal;
font-size: $font-size-micro;
font-weight: $font-weight-normal;
line-height: $space-normal;
margin-left: auto;
}
}
@@ -1,7 +1,108 @@
@mixin bubble-with-tyes {
@include padding($space-smaller $space-one);
@include margin($zero);
background: $color-primary-light;
border-radius: $space-small;
color: $color-heading;
font-size: $font-size-small;
position: relative;
.icon {
bottom: $space-smaller;
position: absolute;
right: $space-small;
}
.message-text__wrap {
position: relative;
}
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
.audio {
.time {
margin-top: -$space-two;
}
}
.image {
@include flex;
align-items: flex-end;
justify-content: center;
text-align: center;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
margin-left: -$space-large;
white-space: nowrap;
}
.modal-image {
max-height: 80%;
max-width: 80%;
}
}
.map {
@include flex;
flex-direction: column;
text-align: right;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
@include padding($space-small);
margin-left: -$space-smaller;
margin-top: -$space-two;
white-space: nowrap;
}
.locname {
font-weight: $font-weight-medium;
padding: $space-smaller;
}
}
}
.conversations-sidebar {
@include flex;
flex-direction: column;
.load-more-conversations {
color: $color-woot;
cursor: pointer;
font-size: $font-size-small;
padding: $space-normal;
&:hover {
background: $color-background;
}
}
.end-of-list-text {
font-style: italic;
padding: $space-normal;
}
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
}
.chat-list__top {
@include flex;
@include padding($space-normal $zero $space-small $zero);
@@ -28,10 +129,7 @@
}
}
.conversations-list {
@include flex-weight(1);
@include scroll-on-hover;
}
.content-box {
text-align: center;
@@ -47,16 +145,19 @@
@include background-gray;
@include margin(0);
@include border-normal-left;
.current-chat {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
div {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
img {
@include margin($space-normal);
width: 10rem;
@@ -73,21 +174,22 @@
.conv-empty-state {
@include flex;
@include full-height;
flex-direction: column;
@include flex-align(center, middle);
flex-direction: column;
}
}
.conversation-panel {
@include flex;
@include flex-weight(1);
flex-direction: column;
@include margin($zero);
flex-direction: column;
// Firefox flexbox fix
height: 100%;
overflow-y: scroll;
margin-bottom: $space-small;
overflow-y: auto;
> li {
>li {
@include flex;
@include flex-shrink;
@include margin($zero $zero $space-smaller);
@@ -114,6 +216,7 @@
}
.bubble {
@include bubble-with-tyes;
max-width: 50rem;
text-align: left;
word-wrap: break-word;
@@ -147,7 +250,7 @@
@include flex-align(right, null);
.wrap {
margin-right: $space-small;
margin-right: $space-normal;
text-align: right;
}
@@ -205,6 +308,7 @@
@include padding($space-smaller $space-normal);
@include flex-align($x: center, $y: null);
background: lighten($warning-color, 32%);
border: 1px solid lighten($warning-color, 26%);
border-radius: $space-smaller;
font-size: $font-size-small;
@@ -237,87 +341,6 @@
}
}
.bubble {
@include padding($space-smaller $space-one);
@include margin($zero);
background: #c7e3ff;
border-radius: $space-small;
box-shadow: 0 .5px .5px rgba(0, 0, 0, .05);
color: $color-heading;
font-size: $font-size-small;
position: relative;
.icon {
bottom: $space-smaller;
position: absolute;
right: $space-small;
}
.message-text__wrap {
position: relative;
}
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
.audio {
.time {
margin-top: -$space-two;
}
}
.image {
@include flex;
justify-content: center;
align-items: flex-end;
text-align: center;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
margin-left: -$space-large;
white-space: nowrap;
}
.modal-image {
max-height: 80%;
max-width: 80%;
}
}
.map {
@include flex;
flex-direction: column;
text-align: right;
img {
@include padding($space-small);
max-height: 30rem;
max-width: 20rem;
}
.time {
@include padding($space-small);
margin-left: -$space-smaller;
margin-top: -$space-two;
white-space: nowrap;
}
.locname {
font-weight: $font-weight-medium;
padding: $space-smaller;
}
}
}
.time {
bottom: -$space-micro;
color: $color-gray;
@@ -23,7 +23,7 @@
@include padding($space-small);
box-sizing: border-box;
height: 180px;
overflow-y: scroll;
overflow-y: auto;
.emoji {
border-radius: 4px;
@@ -5,7 +5,7 @@
// Outside login wrapper
.login {
@include full-height;
overflow-y: scroll;
overflow-y: auto;
padding-top: $space-larger * 1.2;
.login__hero {
@@ -40,7 +40,7 @@
background-color: $color-white;
border-radius: $space-small;
max-height: 100%;
overflow: scroll;
overflow: auto;
position: relative;
width: 60rem;
@@ -1,10 +1,10 @@
.reply-box {
@include elegant-card;
border-bottom: 0;
margin: $space-normal;
margin-top: 0;
border-bottom: 0;
@include elegant-card;
transition: height 2s $ease-in-out-cubic;
max-height: $space-jumbo * 2;
transition: height 2s $ease-in-out-cubic;
.reply-box__top {
@include flex;
@@ -12,26 +12,25 @@
@include padding($space-one $space-normal);
@include background-white;
@include margin(0);
position: relative;
border-top-left-radius: $space-small;
border-top-right-radius: $space-small;
position: relative;
.canned {
@include elegant-card;
z-index: 100;
position: absolute;
background: #fff;
width: 24rem;
left: 0;
border-top: $space-small solid $color-white;
background: $color-white;
border-bottom: $space-small solid $color-white;
max-height: 14rem;
overflow: scroll;
border-top: $space-small solid $color-white;
left: 0;
.active {
a {
background: $color-woot;
}
max-height: 14rem;
overflow: auto;
position: absolute;
width: 24rem;
z-index: 100;
.active a {
background: $color-woot;
}
}
@@ -43,30 +42,30 @@
&.is-private {
background: lighten($warning-color, 38%);
> input {
>input {
background: lighten($warning-color, 38%);
}
}
> .icon {
font-size: $font-size-medium;
>.icon {
color: $medium-gray;
margin-right: $space-small;
cursor: pointer;
font-size: $font-size-medium;
margin-right: $space-small;
&.active {
color: $color-woot;
}
}
> textarea {
>textarea {
@include ghost-input();
@include margin(0);
resize: none;
background: transparent;
// Override min-height : 50px in foundation
//
min-height: 1rem;
resize: none;
}
}
@@ -80,48 +79,47 @@
.tabs {
border: 0;
padding: 0;
flex: 1;
padding: 0;
.tabs-title {
margin: 0;
transition: background .2s $ease-in-out-cubic;
transition: color .2s $ease-in-out-cubic;
transition: all .2s $ease-in-out-cubic;
transition-property: color, background;
a {
font-weight: $font-weight-medium;
padding: $space-one $space-two;
}
&:first-child {
border-bottom-left-radius: $space-small;
&.is-private.is-active {
background: lighten($warning-color, 38%);
&.is-active {
@include border-light-right;
border-left: 0;
a {
border-bottom-left-radius: $space-small;
}
a {
border-bottom-color: darken($warning-color, 15%);
color: darken($warning-color, 15%);
}
}
}
&.is-private {
&.is-active {
background: lighten($warning-color, 38%);
.tabs-title:first-child {
border-bottom-left-radius: $space-small;
a {
border-bottom-color: darken($warning-color, 15%);
color: darken($warning-color, 15%);
}
&.is-active {
@include border-light-right;
border-left: 0;
a {
border-bottom-left-radius: $space-small;
}
}
}
.is-active {
@include background-white;
margin-top: -1px;
@include border-light-left;
@include border-light-right;
margin-top: -1px;
}
.message-length {
@@ -138,11 +136,11 @@
}
.send-button {
height: 3.6rem;
border-bottom-right-radius: $space-small;
padding-top: $space-small;
padding-right: $space-two;
height: 3.6rem;
padding-left: $space-two;
padding-right: $space-two;
padding-top: $space-small;
.icon {
margin-left: $space-small;
@@ -1,7 +1,7 @@
.side-menu {
i {
min-width: $space-two;
margin-right: $space-smaller;
min-width: $space-two;
}
}
@@ -27,6 +27,26 @@
border-radius: $space-smaller;
color: $color-gray;
font-size: $font-size-default;
font-weight: $font-weight-medium;
}
.active a {
color: $color-woot;
}
}
.nested {
a {
font-size: $font-size-small;
margin-bottom: $space-micro;
margin-top: $space-micro;
>.inbox-icon {
display: inline-block;
margin-right: $space-micro;
min-width: $space-normal;
text-align: center;
}
}
}
}
@@ -36,17 +56,17 @@
@include flex;
@include space-between-column;
@include padding($space-one $space-normal $space-one $space-one);
flex-direction: column;
@include border-normal-top;
flex-direction: column;
position: relative;
.dropdown-pane {
@include elegant-card;
@include border-light;
display: block;
left: 18%;
top: -110%;
visibility: visible;
display: block;
width: 80%;
z-index: 999;
@@ -79,23 +99,23 @@
font-size: $font-size-medium;
margin-top: $space-medium;
> span {
>span {
margin-left: auto;
}
}
}
.menu-title + ul > li > a {
.menu-title+ul>li>a {
@include padding($space-micro null);
color: $medium-gray;
line-height: $global-lineheight;
}
.current-user {
align-items: center;
@include flex;
flex-direction: row;
align-items: center;
cursor: pointer;
flex-direction: row;
.current-user--data {
@include flex;
@@ -105,7 +125,7 @@
font-size: $font-size-small;
font-weight: $font-weight-medium;
line-height: 1;
margin-bottom: $zero;
margin-bottom: $space-smaller;
margin-left: $space-one;
margin-top: $space-micro;
}
@@ -132,7 +152,7 @@
display: none;
margin-right: $space-normal;
@media screen and (max-width: 1200px){
@media screen and (max-width: 1200px) {
display: block;
}
}
@@ -141,7 +161,7 @@
display: block;
margin-right: $space-normal;
@media screen and (max-width: 1200px){
@media screen and (max-width: 1200px) {
display: none;
}
}
@@ -13,6 +13,7 @@
.tabs-title {
a {
font-size: $font-size-default;
font-weight: $font-weight-medium;
padding-bottom: $space-slab;
padding-top: $space-slab;
}
@@ -3,40 +3,52 @@
<div class="chat-list__top">
<h1 class="page-title">
<woot-sidemenu-icon />
{{ inbox.name || pageTitle }}
{{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}
</h1>
<chat-filter @statusFilterChange="getDataForStatusTab" />
<chat-filter @statusFilterChange="updateStatusType" />
</div>
<chat-type-tabs
:items="assigneeTabItems"
:active-tab-index="activeAssigneeTab"
:active-tab="activeAssigneeTab"
class="tab--chat-type"
@chatTabChange="getDataForTab"
@chatTabChange="updateAssigneeTab"
/>
<p
v-if="!chatListLoading && !getChatsForTab(activeStatus).length"
class="content-box"
>
<p v-if="!chatListLoading && !getChatsForTab().length" class="content-box">
{{ $t('CHAT_LIST.LIST.404') }}
</p>
<div v-if="chatListLoading" class="text-center">
<span class="spinner message"></span>
</div>
<transition-group
name="conversations-list"
tag="div"
class="conversations-list"
>
<div class="conversations-list">
<conversation-card
v-for="chat in getChatsForTab(activeStatus)"
v-for="chat in getChatsForTab()"
:key="chat.id"
:chat="chat"
/>
</transition-group>
<div v-if="chatListLoading" class="text-center">
<span class="spinner"></span>
</div>
<div
v-if="!hasCurrentPageEndReached && !chatListLoading"
class="text-center load-more-conversations"
@click="fetchConversations"
>
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
</div>
<p
v-if="
getChatsForTab().length &&
hasCurrentPageEndReached &&
!chatListLoading
"
class="text-center text-muted end-of-list-text"
>
{{ $t('CHAT_LIST.EOF') }}
</p>
</div>
</div>
</template>
@@ -59,11 +71,11 @@ export default {
ChatFilter,
},
mixins: [timeMixin, conversationMixin],
props: ['conversationInbox', 'pageTitle'],
props: ['conversationInbox'],
data() {
return {
activeAssigneeTab: 0,
activeStatus: 0,
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
};
},
computed: {
@@ -78,66 +90,69 @@ export default {
convStats: 'getConvTabStats',
}),
assigneeTabItems() {
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map((item, index) => ({
id: index,
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({
key: item.KEY,
name: item.NAME,
count: this.convStats[item.KEY] || 0,
count: this.convStats[item.COUNT_KEY] || 0,
}));
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
},
getToggleStatus() {
if (this.toggleType) {
return 'Open';
}
return 'Resolved';
currentPage() {
return this.$store.getters['conversationPage/getCurrentPage'](
this.activeAssigneeTab
);
},
hasCurrentPageEndReached() {
return this.$store.getters['conversationPage/getHasEndReached'](
this.activeAssigneeTab
);
},
},
watch: {
conversationInbox() {
this.resetAndFetchData();
},
},
mounted() {
this.$watch('$store.state.route', () => {
if (this.$store.state.route.name !== 'inbox_conversation') {
this.$store.dispatch('emptyAllConversations');
this.fetchData();
}
});
this.$store.dispatch('emptyAllConversations');
this.fetchData();
this.$store.dispatch('setChatFilter', this.activeStatus);
this.resetAndFetchData();
this.$store.dispatch('agents/get');
},
methods: {
fetchData() {
if (this.chatLists.length === 0) {
this.fetchConversations();
}
resetAndFetchData() {
this.$store.dispatch('conversationPage/reset');
this.$store.dispatch('emptyAllConversations');
this.fetchConversations();
},
fetchConversations() {
this.$store.dispatch('fetchAllConversations', {
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
assigneeType: this.activeAssigneeTab,
status: this.activeStatus ? 'resolved' : 'open',
status: this.activeStatus,
page: this.currentPage + 1,
});
},
getDataForTab(index) {
if (this.activeAssigneeTab !== index) {
this.activeAssigneeTab = index;
this.fetchConversations();
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
this.activeAssigneeTab = selectedTab;
if (!this.currentPage) {
this.fetchConversations();
}
}
},
getDataForStatusTab(index) {
updateStatusType(index) {
if (this.activeStatus !== index) {
this.activeStatus = index;
this.fetchConversations();
this.resetAndFetchData();
}
},
getChatsForTab() {
let copyList = [];
if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.MINE) {
if (this.activeAssigneeTab === 'me') {
copyList = this.mineChatsList.slice();
} else if (
this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.UNASSIGNED
) {
} else if (this.activeAssigneeTab === 'unassigned') {
copyList = this.unAssignedChatsList.slice();
} else {
copyList = this.allChatList.slice();
+23 -3
View File
@@ -1,6 +1,11 @@
<template>
<transition name="modal-fade">
<div v-if="show" class="modal-mask" transition="modal" @click="close">
<div
v-if="show"
class="modal-mask"
transition="modal"
@click="onBackDropClick"
>
<i class="ion-android-close modal--close" @click="close"></i>
<div class="modal-container" :class="className" @click.stop>
<slot />
@@ -12,9 +17,19 @@
<script>
export default {
props: {
closeOnBackdropClick: {
type: Boolean,
default: true,
},
show: Boolean,
onClose: Function,
className: String,
onClose: {
type: Function,
required: true,
},
className: {
type: String,
default: '',
},
},
mounted() {
document.addEventListener('keydown', e => {
@@ -27,6 +42,11 @@ export default {
close() {
this.onClose();
},
onBackDropClick() {
if (this.closeOnBackdropClick) {
this.onClose();
}
},
},
};
</script>
@@ -16,8 +16,12 @@
/* global bus */
import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner';
import wootConstants from '../../constants';
export default {
components: {
Spinner,
},
props: ['conversationId'],
data() {
return {
@@ -29,19 +33,23 @@ export default {
currentChat: 'getSelectedChat',
}),
currentStatus() {
const ButtonName = this.currentChat.status === 0 ? 'Resolve' : 'Reopen';
const ButtonName =
this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? this.$t('CONVERSATION.HEADER.RESOLVE_ACTION')
: this.$t('CONVERSATION.HEADER.REOPEN_ACTION');
return ButtonName;
},
buttonClass() {
return this.currentChat.status === 0 ? 'success' : 'warning';
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? 'success'
: 'warning';
},
buttonIconClass() {
return this.currentChat.status === 0 ? 'ion-checkmark' : 'ion-refresh';
return this.currentChat.status === wootConstants.STATUS_TYPE.OPEN
? 'ion-checkmark'
: 'ion-refresh';
},
},
components: {
Spinner,
},
methods: {
toggleStatus() {
this.isLoading = true;
@@ -42,12 +42,21 @@
class="dropdown-pane top"
>
<ul class="vertical dropdown menu">
<li><a href="#" @click.prevent="logout()">Logout</a></li>
<li>
<router-link to="/app/profile/settings">
{{ $t('SIDEBAR.PROFILE_SETTINGS') }}
</router-link>
</li>
<li>
<a href="#" @click.prevent="logout()">
{{ $t('SIDEBAR.LOGOUT') }}
</a>
</li>
</ul>
</div>
</transition>
<div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="gravatarUrl()" :username="currentUser.name" />
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
<div class="current-user--data">
<h3 class="current-user--name">
{{ currentUser.name }}
@@ -65,7 +74,6 @@
<script>
import { mapGetters } from 'vuex';
import md5 from 'md5';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin';
@@ -99,6 +107,7 @@ export default {
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes',
currentUser: 'getCurrentUser',
}),
accessibleMenuItems() {
// get all keys in menuGroup
@@ -141,12 +150,10 @@ export default {
id: inbox.id,
label: inbox.name,
toState: frontendURL(`inbox/${inbox.id}`),
type: inbox.channel_type,
})),
};
},
currentUser() {
return Auth.getCurrentUser();
},
dashboardPath() {
return frontendURL('dashboard');
},
@@ -174,10 +181,6 @@ export default {
this.$store.dispatch('inboxes/get');
},
methods: {
gravatarUrl() {
const hash = md5(this.currentUser.email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
},
filterBillingRoutes(menuItems) {
return menuItems.filter(
menuItem => !menuItem.toState.includes('billing')
@@ -185,6 +188,9 @@ export default {
},
filterMenuItemsByRole(menuItems) {
const { role } = this.currentUser;
if (!role) {
return [];
}
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
@@ -24,11 +24,17 @@
v-for="child in menuItem.children"
:key="child.id"
active-class="active flex-container"
:class="computedInboxClass(child)"
tag="li"
:to="child.toState"
>
<a>{{ child.label }}</a>
<a href="#">
<i
v-if="computedInboxClass(child)"
class="inbox-icon"
:class="computedInboxClass(child)"
></i>
{{ child.label }}
</a>
</router-link>
</ul>
</router-link>
@@ -41,6 +47,27 @@ import { mapGetters } from 'vuex';
import router from '../../routes';
import auth from '../../api/auth';
const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
};
const getInboxClassByType = type => {
switch (type) {
case INBOX_TYPES.WEB:
return 'ion-earth';
case INBOX_TYPES.FB:
return 'ion-social-facebook';
case INBOX_TYPES.TWITTER:
return 'ion-social-twitter';
default:
return '';
}
};
export default {
props: {
menuItem: {
@@ -75,10 +102,9 @@ export default {
},
methods: {
computedInboxClass(child) {
if (parseInt(this.activeInbox, 10) === child.channel_id) {
return 'active flex-container';
}
return ' ';
const { type } = child;
const classByType = getInboxClassByType(type);
return classByType;
},
newLinkClick() {
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
@@ -14,15 +14,19 @@ export default {
props: {
username: {
type: String,
default: '',
},
backgroundColor: {
type: String,
default: 'white',
},
color: {
type: String,
default: '#1f93ff',
},
customStyle: {
type: Object,
default: undefined,
},
size: {
type: Number,
@@ -30,6 +34,7 @@ export default {
},
src: {
type: String,
default: '',
},
rounded: {
type: Boolean,
@@ -72,7 +77,7 @@ export default {
<style lang="scss" scoped>
.avatar-container {
display: flex;
font-weight: bold;
font-weight: 500;
align-items: center;
justify-content: center;
text-align: center;
@@ -39,7 +39,7 @@ export default {
},
methods: {
isActive(channel) {
return ['facebook', 'website'].includes(channel);
return ['facebook', 'website', 'twitter'].includes(channel);
},
onItemClick() {
if (this.isActive(this.channel)) {
@@ -1,15 +1,15 @@
<template>
<woot-tabs :index="tabsIndex" @change="onTabChange">
<woot-tabs :index="activeTabIndex" @change="onTabChange">
<woot-tabs-item
v-for="item in items"
:key="item.name"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</template>
<script>
/* eslint no-console: 0 */
import wootConstants from '../../constants';
export default {
props: {
@@ -17,24 +17,25 @@ export default {
type: Array,
default: () => [],
},
activeTabIndex: {
type: Number,
default: 0,
activeTab: {
type: String,
default: wootConstants.ASSIGNEE_TYPE.ME,
},
},
data() {
return {
tabsIndex: 0,
tabsIndex: wootConstants.ASSIGNEE_TYPE.ME,
};
},
created() {
this.tabsIndex = this.activeTabIndex;
computed: {
activeTabIndex() {
return this.items.findIndex(item => item.key === this.activeTab);
},
},
methods: {
onTabChange(selectedTabIndex) {
if (selectedTabIndex !== this.tabsIndex) {
this.$emit('chatTabChange', selectedTabIndex);
this.tabsIndex = selectedTabIndex;
if (this.items[selectedTabIndex].key !== this.activeTab) {
this.$emit('chatTabChange', this.items[selectedTabIndex].key);
}
},
},
@@ -4,19 +4,19 @@
v-if="!imgError && Boolean(src)"
id="image"
:src="src"
class="user-thumbnail"
:class="thumbnailClass"
@error="onImgError()"
/>
<Avatar
v-else
:username="username"
class="user-thumbnail"
:class="thumbnailClass"
background-color="#1f93ff"
color="white"
:size="avatarSize"
/>
<img
v-if="badge === 'Channel::FacebookPage' && status !== ''"
v-if="badge === 'Channel::FacebookPage'"
id="badge"
class="source-badge"
:style="badgeStyle"
@@ -28,7 +28,7 @@
:style="statusStyle"
></div>
<img
v-if="badge === 'Channel::TwitterProfile' && status !== ''"
v-if="badge === 'Channel::TwitterProfile'"
id="badge"
class="source-badge"
:style="badgeStyle"
@@ -71,6 +71,10 @@ export default {
type: String,
default: '',
},
hasBorder: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -89,6 +93,10 @@ export default {
const statusSize = `${this.avatarSize / 4}px`;
return { width: statusSize, height: statusSize };
},
thumbnailClass() {
const classname = this.hasBorder ? 'border' : '';
return `user-thumbnail ${classname}`;
},
},
methods: {
onImgError() {
@@ -111,6 +119,11 @@ export default {
border-radius: 50%;
height: 100%;
width: 100%;
box-sizing: border-box;
&.border {
border: 1px solid white;
}
}
.source-badge {
@@ -1,5 +1,5 @@
<template>
<select v-model="activeIndex" class="status--filter" @change="onTabChange()">
<select v-model="activeStatus" class="status--filter" @change="onTabChange()">
<option
v-for="item in $t('CHAT_LIST.CHAT_STATUS_ITEMS')"
:key="item['VALUE']"
@@ -11,15 +11,16 @@
</template>
<script>
import wootConstants from '../../../constants';
export default {
data: () => ({
activeIndex: 0,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
}),
mounted() {},
methods: {
onTabChange() {
this.$store.dispatch('setChatFilter', this.activeIndex);
this.$emit('statusFilterChange', this.activeIndex);
this.$store.dispatch('setChatFilter', this.activeStatus);
this.$emit('statusFilterChange', this.activeStatus);
},
},
};
@@ -46,7 +46,7 @@ import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL } from '../../../helper/URLHelper';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
export default {
components: {
@@ -95,9 +95,9 @@ export default {
methods: {
cardClick(chat) {
router.push({
path: frontendURL(`conversations/${chat.id}`),
});
const { activeInbox } = this;
const path = conversationUrl(activeInbox, chat.id);
router.push({ path: frontendURL(path) });
},
extractMessageText(chatItem) {
if (chatItem.content) {
@@ -93,7 +93,11 @@ export default {
];
},
viewProfileButtonLabel() {
return `${this.isContactPanelOpen ? 'Hide' : 'View'} Profile`;
return `${
this.isContactPanelOpen
? this.$t('CONVERSATION.HEADER.CLOSE')
: this.$t('CONVERSATION.HEADER.OPEN')
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
},
},
@@ -22,7 +22,6 @@
:label="data.attachment.fallback_title"
:readable-time="readableTime"
/>
<i v-if="data.message_type === 2" class="icon ion-person" />
<bubble-text
v-if="data.content"
:message="message"
@@ -33,14 +33,18 @@
<div class="reply-box__bottom">
<ul class="tabs">
<li class="tabs-title" :class="{ 'is-active': !isPrivate }">
<a href="#" @click="makeReply">Reply</a>
<a href="#" @click="makeReply">{{
$t('CONVERSATION.REPLYBOX.REPLY')
}}</a>
</li>
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
<a href="#" @click="makePrivate">Private Note</a>
<a href="#" @click="makePrivate">{{
$t('CONVERSATION.REPLYBOX.PRIVATE_NOTE')
}}</a>
</li>
<li v-if="message.length" class="tabs-title message-length">
<a :class="{ 'message-error': message.length > 620 }">
{{ message.length }} / 640
<a :class="{ 'message-error': message.length > maxLength - 40 }">
{{ message.length }} / {{ maxLength }}
</a>
</li>
</ul>
@@ -49,16 +53,12 @@
class="button send-button"
:disabled="disableButton()"
:class="{
disabled: message.length === 0 || message.length > 640,
disabled: message.length === 0 || message.length > maxLength,
warning: isPrivate,
}"
@click="sendMessage"
>
{{
isPrivate
? $t('CONVERSATION.REPLYBOX.CREATE')
: $t('CONVERSATION.REPLYBOX.SEND')
}}
{{ replyButtonLabel }}
<i
class="icon"
:class="{
@@ -82,6 +82,10 @@ import EmojiInput from '../emoji/EmojiInput';
import CannedResponse from './CannedResponse';
export default {
components: {
EmojiInput,
CannedResponse,
},
mixins: [clickaway],
data() {
return {
@@ -103,10 +107,31 @@ export default {
} = this.currentChat;
return channel;
},
},
components: {
EmojiInput,
CannedResponse,
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
return type || '';
},
maxLength() {
if (this.channelType === 'Channel::FacebookPage') {
return 640;
}
if (this.channelType === 'Channel::TwitterProfile') {
if (this.conversationType === 'tweet') {
return 280;
}
}
return 10000;
},
replyButtonLabel() {
if (this.isPrivate) {
return this.$t('CONVERSATION.REPLYBOX.CREATE');
}
if (this.conversationType === 'tweet') {
return this.$t('CONVERSATION.REPLYBOX.TWEET');
}
return this.$t('CONVERSATION.REPLYBOX.SEND');
},
},
watch: {
message(val) {
+8 -4
View File
@@ -4,9 +4,13 @@ export default {
return `${this.APP_BASE_URL}/`;
},
GRAVATAR_URL: 'https://www.gravatar.com/avatar/',
ASSIGNEE_TYPE_SLUG: {
MINE: 0,
UNASSIGNED: 1,
OPEN: 0,
ASSIGNEE_TYPE: {
ME: 'me',
UNASSIGNED: 'unassigned',
ALL: 'all',
},
STATUS_TYPE: {
OPEN: 'open',
RESOLVED: 'resolved',
},
};
@@ -4,3 +4,10 @@ export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`;
};
export const conversationUrl = (activeInbox, id) => {
const path = activeInbox
? `inbox/${activeInbox}/conversations/${id}`
: `conversations/${id}`;
return path;
};
@@ -0,0 +1,21 @@
import { frontendURL, conversationUrl } from '../URLHelper';
describe('#URL Helpers', () => {
describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl(undefined, 1)).toBe('conversations/1');
});
it('should return ibox conversation URL if activeInbox is not nil', () => {
expect(conversationUrl(2, 1)).toBe('inbox/2/conversations/1');
});
});
describe('frontendURL', () => {
it('should return url without params if params passed is nil', () => {
expect(frontendURL('main', null)).toBe('/app/main');
});
it('should return url without params if params passed is not nil', () => {
expect(frontendURL('main', { ping: 'pong' })).toBe('/app/main?ping=pong');
});
});
});
@@ -6,8 +6,11 @@ export default {
'home',
'inbox_dashboard',
'inbox_conversation',
'conversation_through_inbox',
'settings_account_reports',
'billing_deactivated',
'profile_settings',
'profile_settings_index',
],
menuItems: {
assignedToMe: {
@@ -49,6 +52,8 @@ export default {
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
'settings_integrations',
'settings_integrations_webhook',
],
menuItems: {
back: {
@@ -86,12 +91,12 @@ export default {
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
settings_integrations: {
icon: 'ion-flash',
label: 'Integrations',
hasSubMenu: false,
toState: frontendURL('settings/account'),
toStateName: 'account',
toState: frontendURL('settings/integrations'),
toStateName: 'settings_integrations',
},
},
},
@@ -45,6 +45,7 @@
},
"API": {
"SUCCESS_MESSAGE": "Agent added successfully",
"EXIST_MESSAGE": "Agent email already in use, Please try another email address",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
@@ -58,7 +59,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
}
},
"EDIT": {
@@ -67,7 +67,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
}
}
}
@@ -1,6 +1,8 @@
{
"CHAT_LIST": {
"LOADING": "Fetching conversations",
"LOAD_MORE_CONVERSATIONS": "Load more conversations...",
"EOF": "You have reached the end of the list",
"LIST": {
"404": "There are no active conversations in this group."
},
@@ -14,20 +16,14 @@
],
"ASSIGNEE_TYPE_TABS": [
{ "NAME": "Mine", "KEY": "mineCount"},
{ "NAME": "Unassigned", "KEY": "unAssignedCount"},
{ "NAME": "All", "KEY": "allCount"}
{ "NAME": "Mine", "KEY": "me", "COUNT_KEY": "mineCount" },
{ "NAME": "Unassigned", "KEY": "unassigned", "COUNT_KEY": "unAssignedCount"},
{ "NAME": "All", "KEY": "all", "COUNT_KEY": "allCount" }
],
"ASSIGNEE_TYPE_SLUG": {
"MINE": 0,
"UNASSIGNED": 1,
"ALL": 2
},
"CHAT_STATUS_ITEMS": [
{ "TEXT": "Open", "VALUE": 0 },
{ "TEXT": "Resolved", "VALUE": 1 }
{ "TEXT": "Open", "VALUE": "open" },
{ "TEXT": "Resolved", "VALUE": "resolved" }
],
"ATTACHMENTS": {
@@ -1,5 +1,6 @@
{
"CONTACT_PANEL": {
"CONVERSATION_TITLE": "Conversation Details",
"BROWSER": "Browser",
"OS": "Operating System",
"INITIATED_FROM": "Initiated from",
@@ -9,8 +10,11 @@
"TITLE": "Previous Conversations"
},
"LABELS": {
"NO_RECORDS_FOUND": "There are no labels associated to this conversation.",
"TITLE": "Labels"
"TITLE": "Conversation Labels",
"UPDATE_BUTTON": "Update Labels",
"UPDATE_ERROR": "Couldn't update labels, try again.",
"TAG_PLACEHOLDER": "Add new label",
"PLACEHOLDER": "Search or add a label"
}
}
}
@@ -10,15 +10,22 @@
"LOADING_INBOXES": "Loading inboxes",
"LOADING_CONVERSATIONS": "Loading Conversations",
"HEADER": {
"RESOLVE_ACTION": "Resolve"
"RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen",
"OPEN": "More",
"CLOSE": "Close",
"DETAILS": "details"
},
"FOOTER": {
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
},
"REPLYBOX": {
"REPLY": "Reply",
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note"
"CREATE": "Add Note",
"TWEET": "Tweet"
},
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
"CHANGE_STATUS": "Conversation status changed",
@@ -15,6 +15,9 @@
"FB": {
"HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' "
},
"WEBSITE_CHANNEL": {
"TITLE": "Website channel",
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",
@@ -34,11 +37,11 @@
},
"AUTH": {
"TITLE": "Channels",
"DESC": "Currently we support website live chat widgets and Facebook Pages as platforms. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon."
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
},
"AGENTS": {
"TITLE": "Agents",
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents whcih are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents which are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
},
"DETAILS": {
"TITLE": "Inbox Details",
@@ -71,7 +74,12 @@
"EDIT": {
"API": {
"SUCCESS_MESSAGE": "Widget color updated successfully",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later."
},
"AUTO_ASSIGNMENT": {
"ENABLED": "Enabled",
"DISABLED": "Disabled"
}
},
"DELETE": {
@@ -80,7 +88,7 @@
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep "
"NO": "No, Keep it "
},
"API": {
"SUCCESS_MESSAGE": "Inbox deleted successfully",
@@ -93,7 +101,9 @@
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
"INBOX_AGENTS": "Agents",
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"UPDATE": "Update"
"UPDATE": "Update",
"AUTO_ASSIGNMENT": "Enable auto assignment",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of available agents on new conversations"
}
}
}
@@ -10,7 +10,9 @@ import { default as _login } from './login.json';
import { default as _report } from './report.json';
import { default as _resetPassword } from './resetPassword.json';
import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json';
import { default as _integrations } from './integrations.json';
export default {
..._agentMgmt,
@@ -24,5 +26,7 @@ export default {
..._report,
..._resetPassword,
..._setNewPassword,
..._settings,
..._signup,
..._integrations,
};
@@ -0,0 +1,54 @@
{
"INTEGRATION_SETTINGS": {
"HEADER": "Integrations",
"WEBHOOK": {
"TITLE": "Webhook",
"CONFIGURE": "Configure",
"HEADER": "Webhook settings",
"HEADER_BTN_TXT": "Add new webhook",
"INTEGRATION_TXT": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks.",
"LOADING": "Fetching attached webhooks",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Webhooks</b> </p> <p>Webhooks are HTTP callbacks which can be defined for every account. They are triggered by events like message creation in Chatwoot. You can create more than one webhook for this account. <br /><br /> For creating a <b>webhook</b>, click on the <b>Add new webhook</b> button. You can also remove any existing webhook by clicking on the Delete button.</p>",
"LIST": {
"404": "There are no webhooks configured for this account.",
"TITLE": "Manage webhooks",
"DESC": "Webhooks are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": [
"Webhook endpoint",
"Actions"
]
},
"ADD": {
"CANCEL": "Cancel",
"TITLE": "Add new webhook",
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
"FORM": {
"END_POINT": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Example: https://example/api/webhook",
"ERROR": "Please enter a valid URL"
},
"SUBMIT": "Create webhook"
},
"API": {
"SUCCESS_MESSAGE": "Webhook added successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"API": {
"SUCCESS_MESSAGE": "Webhook deleted successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
},
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ",
"NO": "No, Keep it "
}
}
}
}
}
@@ -0,0 +1,58 @@
{
"PROFILE_SETTINGS": {
"LINK": "Profile Settings",
"TITLE": "Profile Settings",
"BTN_TEXT": "Update Profile",
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
"FORM": {
"AVATAR": "Profile Image",
"ERROR": "Please fix form errors",
"REMOVE_IMAGE": "Remove",
"UPLOAD_IMAGE": "Upload image",
"UPDATE_IMAGE": "Update image",
"PROFILE_SECTION" : {
"TITLE": "Profile",
"NOTE": "Your email address is your identity and is used to log in."
},
"PASSWORD_SECTION" : {
"TITLE": "Password",
"NOTE": "Updating your password would reset your logins in multiple devices."
},
"EMAIL_NOTIFICATIONS_SECTION" : {
"TITLE": "Email Notifications",
"NOTE": "Update your email notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
"UPDATE_SUCCESS": "Your email notification preferences are updated successfully",
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
},
"PROFILE_IMAGE":{
"LABEL": "Profile Image"
},
"NAME": {
"LABEL": "Your name",
"ERROR": "Please enter a valid name",
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
},
"EMAIL": {
"LABEL": "Your email address",
"ERROR": "Please enter a valid email address",
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
},
"PASSWORD": {
"LABEL": "Password",
"ERROR": "Please enter a password of length 6 or more",
"PLACEHOLDER": "Please enter a new password"
},
"PASSWORD_CONFIRMATION": {
"LABEL": "Confirm new password",
"ERROR": "Confirm password should match the password",
"PLACEHOLDER": "Please re-enter your password"
}
}
},
"SIDEBAR": {
"PROFILE_SETTINGS": "Profile Settings",
"LOGOUT": "Logout"
}
}
@@ -1,7 +1,6 @@
{
"REGISTER": {
"TRY_WOOT": "Try Chatwoot free for 14 days",
"TRY_WOOT_SUB": "No credit card required. Cancel anytime.",
"TRY_WOOT": "Register an account",
"TITLE": "Register",
"TERMS_ACCEPT": "By signing up, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>",
"ACCOUNT_NAME": {
@@ -30,4 +29,4 @@
},
"SUBMIT": "Submit"
}
}
}
@@ -0,0 +1,5 @@
{
"WEBHOOKS_SETTINGS": {
"HEADER": "Webhook Settings"
}
}
@@ -11,7 +11,8 @@ export default {
return m.messages.filter(
chat =>
chat.created_at * 1000 > m.agent_last_seen_at * 1000 &&
(chat.message_type === 0 && chat.private !== true)
chat.message_type === 0 &&
chat.private !== true
).length;
},
readMessages(m) {
@@ -1,23 +1,40 @@
<template>
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="login()">
<form
class="login-box medium-4 column align-self-middle"
@submit.prevent="login()"
>
<div class="column log-in-form">
<h4>{{$t('SET_NEW_PASSWORD.TITLE')}}</h4>
<label :class="{ 'error': $v.credentials.password.$error }">
{{$t('LOGIN.PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch">
<span class="message" v-if="$v.credentials.password.$error">
{{$t('SET_NEW_PASSWORD.PASSWORD.ERROR')}}
<h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4>
<label :class="{ error: $v.credentials.password.$error }">
{{ $t('LOGIN.PASSWORD.LABEL') }}
<input
v-model.trim="credentials.password"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
@input="$v.credentials.password.$touch"
/>
<span v-if="$v.credentials.password.$error" class="message">
{{ $t('SET_NEW_PASSWORD.PASSWORD.ERROR') }}
</span>
</label>
<label :class="{ 'error': $v.credentials.confirmPassword.$error }">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" v-model.trim="credentials.confirmPassword" @input="$v.credentials.confirmPassword.$touch">
<span class="message" v-if="$v.credentials.confirmPassword.$error">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')}}
<label :class="{ error: $v.credentials.confirmPassword.$error }">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }}
<input
v-model.trim="credentials.confirmPassword"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
@input="$v.credentials.confirmPassword.$touch"
/>
<span v-if="$v.credentials.confirmPassword.$error" class="message">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') }}
</span>
</label>
<woot-submit-button
:disabled="$v.credentials.password.$invalid || $v.credentials.confirmPassword.$invalid || newPasswordAPI.showLoading"
:disabled="
$v.credentials.password.$invalid ||
$v.credentials.confirmPassword.$invalid ||
newPasswordAPI.showLoading
"
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
:loading="newPasswordAPI.showLoading"
button-class="expanded"
@@ -99,7 +116,7 @@ export default {
resetPasswordToken: this.resetPasswordToken,
};
Auth.setNewPassword(credentials)
.then((res) => {
.then(res => {
if (res.status === 200) {
window.location = res.data.redirect_url;
}
@@ -9,9 +9,6 @@
<h2 class="hero--title">
{{ $t('REGISTER.TRY_WOOT') }}
</h2>
<p class="hero--sub">
{{ $t('REGISTER.TRY_WOOT_SUB') }}
</p>
</div>
<div class="row align-center">
<div class="medium-5 column">

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