Compare commits

...
126 Commits
Author SHA1 Message Date
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
Sojan cf56b9560d Merge branch 'release/1.1.0' 2020-02-03 01:01:03 +05:45
a9c304f1ef Feature: Twitter DM Integration (#451)
An initial version of twitter integration

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-02-03 00:54:00 +05:45
Sojan JoseandGitHub a287c86bc4 Bugfix: Fix password reset (#455) 2020-02-02 22:52:38 +05:30
Pranav Raj SandGitHub 0b31e14132 Feature: Add online status to each user (#452)
* Feature: Add online status to each user
* Add OnlineStatusable, add availability status to thumbnail
2020-02-02 22:49:16 +05:45
Pranav Raj SandGitHub 1f4703d25d Chore: Disable brakeman in Codeclimate (#453) 2020-02-02 17:35:03 +05:30
b3264a0d7b Feature: API to get most used labels (#447)
* Add most-used labels API

* Filter conversation by labels

* Move match_all to any query

Co-authored-by: Pranav Raj S <pranavrajs@gmail.com>
2020-02-02 16:29:18 +05:30
Sojan JoseandPranav Raj S 199642d3bd Chore: Render avatar url in validate token method (#448) 2020-01-27 00:59:51 +05:45
Anto DominicandSony Mathew 6325acd183 Feature: Add Sidekiq docker (#443)
* update db:schema due to migration

* reduce default concurrency for redis connection limit

* Allow auto update packages in the development mode without building image

* add sidekiq support to docker-compose

* Pass sidekiq cofig file

* passed the env file in base image rather than separately in rails, web packer and sidekiq in docker-compose
* removed un-necessary changes in schema
* changed concurrency to finer values
* removed default size set in sidekiq redis config
* Added the sidekiq config option in Procfile.dev

Co-authored-by: Sony Mathew <ynos1234@gmail.com>
2020-01-27 00:15:13 +05:45
sony-mathew 8cf135f2db Merge branch 'hotfix/1.0.3' into develop
# Conflicts:
#	config/cable.yml
#	docs/development/environment-setup/docker.md
2020-01-26 22:51:39 +05:45
sony-mathew 6efa994096 Merge branch 'hotfix/1.0.3' - Support for authenticated redis connections 2020-01-26 22:47:00 +05:45
sony-mathew 2168f823a5 [#446] Redis authentication support
Previously we did not support authentication for redis anywhere. Also in the docker compose we were exposing redis port 6379 without any authentication. In the app side for the connections that app server (for storing keys as well as for socket connections made using action cable) and Sidekiq were making to redis server did not support authentication.

With this commit, we support authentication for redis connections from app side and Sidekiq. This is supported in docker-compose as well.

The changes include :
* Added support for new env variable REDIS_PASSWORD
* This redis password is now supported by action cable connections, Sidekiq connections and app side redis connections
* Since Sidekiq did not have an initializer, added an initializer to pass custom config to Sidekiq (for now it's options for redis)
* Changes in docker-compose to pickup a password set in .env file to protect the redis server running in docker
* Added necessary documentation changes in `docker.md` and `environment-variables.md`
2020-01-26 22:46:34 +05:45
Sony MathewandGitHub 1fd4127709 Removed agent dashboard link in conversation mail to user (#445) 2020-01-24 14:31:52 +05:45
Sony MathewandPranav Raj S 04d347f0ee Chore: Changed from email and reply to email for conversation email (#444) 2020-01-24 14:10:52 +05:45
Sony MathewandSojan Jose d4b3ba4baa [#139] Send conversation emails (#442)
* [#139] Delayed emails for conversations

* Added the setex and get methods to Redis wrapper
* Set the priorities for the sidekiq queues
* Was not able to use mailhog for testing email in local, switched back to letter opener and added comments on using the SMTP settings
* Added after create hood in messages to queue the sending of mail after 2 minutes using sidekiq worker and also set the redis key for the conversation to avoid the email sending for every message
* Added the sidekiq worker to send the email and delete the conversation redis key
* Added the mailer and mail template
* mailer sends the last 10 messages along with the new messages from the time it was queued

* Send email only in development or if smtp config is set

* Send email only in development or if smtp config is set
* Set the SMTP_PORT in production variable

* Adding redis to circle CI

* Specs for the conversation email changes

* Added specs for conversation email sidekiq worker
* Added specs for conversation mailer
* Added specs in message model for the after create hook for notify email

* Send emails only when there is a reply from agent

* set development to use mailhog

* Adding comments for using letter opener
2020-01-23 23:14:07 +05:45
Sojan JoseandPranav Raj S 1d3ed016be Chore: Documentation for configuring cloud storage (#439) 2020-01-21 22:28:18 +05:45
Pranav Raj SandSojan Jose 95fb6893b4 Feature: API to list the agents for the inbox (#438)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-01-21 19:56:58 +05:45
Pranav Raj SandSojan Jose a2b025b548 Feature: View conversation labels on sidebar (#436) 2020-01-21 18:57:44 +05:45
Pranav Raj SandSojan Jose 90e678743b Update contributing.md (#434) 2020-01-20 23:48:29 +05:45
Muhsin.KandGitHub 9290893f9d Style: Fix the last message style issue (#433) 2020-01-19 14:17:15 +05:30
Nithin David ThomasandPranav Raj S f7f7a6b9c7 Feature: Widget design update (#426) 2020-01-17 13:36:05 +05:30
Subin T PandSojan Jose cc180b77ce Include only incoming/outgoing messages in reporting (#429)
* Skip activity messages from reporting

* Makes reportable? as access list for reporting
2020-01-14 00:50:18 +05:30
Pranav Raj SandSojan Jose 18bf1a9e62 Ignore case from input email (#424) 2020-01-13 14:25:23 +05:30
Nithin David ThomasandPranav Raj S 7cf19e0b52 Feature: Update design for web widget (#408) 2020-01-13 12:10:40 +05:30
Pranav Raj SandSojan Jose 655c585358 Feature: View a contact's previous conversation (#422)
* Add API to fetch conversations of a contact

* Add conversation list in sidebar
2020-01-13 11:17:03 +05:30
Pranav Raj SandGitHub fc6a8c2601 Bug: Fix Heroku deployment on develop branch(#421) 2020-01-12 19:31:32 +05:30
Sojan 63789e5916 Merge branch 'hotfix/1.0.2' into develop 2020-01-09 14:36:50 +05:30
Sojan JoseandPranav Raj S 722f540b03 [Feature] Email collect message hooks (#331)
- Add email collect hook on creating conversation
- Merge contact if it already exist
2020-01-09 13:06:40 +05:30
Sojan 59d4eaeca7 Merge branch 'hotfix/1.0.1' into develop 2020-01-09 12:01:07 +05:30
Pranav Raj SandMuhsin.K b00e13f9db Docs: Add brancing model (#410) 2020-01-08 23:10:46 +05:30
Sojan JoseandPranav Raj S f875a09fb7 Chore: Switch from Carrierwave to ActiveStorage (#393) 2020-01-07 22:59:17 +05:30
mbronek7andSojan Jose f02d422b6a Move ActionCable Broadcast Calls to sidekiq #154 (#403)
* Move Pusher Calls to sidekiq

* CR changes

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2020-01-06 22:32:41 +05:30
Sojan JoseandPranav Raj S 91ace96acd Chore: Sidekiq ActionCable fix for Development (#405)
- action cable works from sidekiq in development environments
- documentation updates for docker
2020-01-05 23:26:22 +05:30
454 changed files with 14719 additions and 4458 deletions
+7 -6
View File
@@ -12,7 +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/postgres:alpine
- image: circleci/redis:alpine
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
@@ -94,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
@@ -125,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
+1 -1
View File
@@ -10,7 +10,7 @@ plugins:
scss-lint:
enabled: true
brakeman:
enabled: true
enabled: false
checks:
similar-code:
enabled: false
+32 -2
View File
@@ -1,5 +1,19 @@
REDIS_URL=redis://redis:6379
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
# If you are using docker-compose, set this variable's value to be any string,
# which will be the password for the redis service running inside the docker-compose
# to make it secure
REDIS_PASSWORD=
# Postgres Database config variables
POSTGRES_HOST=postgres
@@ -13,10 +27,18 @@ FB_VERIFY_TOKEN=
FB_APP_SECRET=
FB_APP_ID=
#twitter app
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#mail
MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025
SMTP_DOMAIN=chatwoot.com
# if you are running docker-compose, set SMTP_ADDRESS value as "mailhog",
# else set the value as "localhost"
SMTP_ADDRESS=mailhog
SMTP_USERNAME=
SMTP_PASSWORD=
@@ -36,6 +58,14 @@ AWS_REGION=
#sentry
SENTRY_DSN=
#Log settings
LOG_LEVEL=
LOG_SIZE=
# Credentials to access sidekiq dashboard in production
SIDEKIQ_AUTH_USERNAME=
SIDEKIQ_AUTH_PASSWORD=
#### This environment variables are only required in hosted version which has billing
ENABLE_BILLING=
@@ -43,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': ['never']
},
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
+11 -2
View File
@@ -4,21 +4,28 @@ require:
- rubocop-rspec
inherit_from: .rubocop_todo.yml
Metrics/LineLength:
Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
RSpec/ExampleLength:
Max: 15
Documentation:
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
Enabled: false
Style/GlobalVars:
Exclude:
- 'config/initializers/redis.rb'
- 'lib/redis/alfred.rb'
Metrics/BlockLength:
Exclude:
- spec/**/*
- '**/routes.rb'
- 'config/environments/*'
- db/schema.rb
Rails/ApplicationController:
Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -32,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'
+18 -5
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'
@@ -23,6 +24,9 @@ gem 'valid_email2'
gem 'uglifier'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
gem 'azure-storage', require: false
gem 'google-cloud-storage', require: false
gem 'mini_magick'
##-- gems for database --#
@@ -57,7 +61,9 @@ 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'
# facebook client
gem 'koala'
# Random name generator
@@ -66,21 +72,27 @@ gem 'haikunator'
##--- gems for debugging and error reporting ---##
# static analysis
gem 'brakeman'
gem 'scout_apm'
gem 'sentry-raven'
##-- TODO: move these gems to appropriate groups --##
# remove this gem in favor of active storage - github #158
gem 'carrierwave-aws'
##-- 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
@@ -94,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
+150 -97
View File
@@ -1,3 +1,18 @@
GIT
remote: https://github.com/chatwoot/twitty
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:
@@ -66,15 +81,15 @@ GEM
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
attr_extras (6.2.1)
attr_extras (6.2.3)
aws-eventstream (1.0.3)
aws-partitions (1.259.0)
aws-sdk-core (3.86.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)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.27.0)
aws-sdk-kms (1.28.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.1)
@@ -87,15 +102,23 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
azure-core (0.1.15)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6)
azure-storage (0.15.0.preview)
azure-core (~> 0.1)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6, >= 1.6.8)
bcrypt (3.1.13)
bindex (0.8.1)
bootsnap (1.4.5)
msgpack (~> 1.0)
brakeman (4.7.2)
browser (2.7.1)
buftok (0.2.0)
browser (3.0.3)
builder (3.2.4)
bullet (6.0.2)
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundle-audit (0.1.0)
@@ -103,18 +126,8 @@ GEM
bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.1)
carrierwave (2.0.2)
activemodel (>= 5.0.0)
activesupport (>= 5.0.0)
addressable (~> 2.6)
image_processing (~> 1.1)
mimemagic (>= 0.3.0)
mini_mime (>= 0.1.3)
carrierwave-aws (1.4.0)
aws-sdk-s3 (~> 1.0)
carrierwave (>= 0.7, < 2.1)
chargebee (2.7.1)
byebug (11.1.1)
chargebee (2.7.3)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
coderay (1.1.2)
@@ -122,7 +135,9 @@ GEM
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
crass (1.0.5)
crass (1.0.6)
declarative (0.0.10)
declarative-option (0.1.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.7.1)
@@ -136,6 +151,7 @@ GEM
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1)
diff-lcs (1.3)
digest-crc (0.4.1)
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@@ -154,35 +170,58 @@ GEM
factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0)
railties (>= 4.2.0)
faker (2.9.0)
i18n (>= 1.6, < 1.8)
faraday (0.17.1)
faker (2.10.1)
i18n (>= 1.6, < 2)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
ffi (1.11.3)
foreman (0.86.0)
faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0)
ffi (1.12.2)
flag_shih_tzu (0.3.23)
foreman (0.87.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.36.4)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
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.1.1)
http_parser.rb (0.6.0)
httparty (0.17.3)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
i18n (1.7.0)
httpclient (2.8.3)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
image_processing (1.10.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.13, < 3)
inflecto (0.0.2)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
@@ -191,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
@@ -221,48 +260,49 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
memoist (0.16.2)
method_source (0.9.2)
mime-types (3.3)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mimemagic (0.3.3)
mini_magick (4.9.5)
mimemagic (0.3.4)
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.13.0)
minitest (5.14.0)
mock_redis (0.22.0)
msgpack (1.3.1)
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.8)
mini_portile2 (~> 2.4.0)
oauth (0.5.4)
orm_adapter (0.5.0)
os (1.0.1)
parallel (1.19.1)
parser (2.6.5.0)
parser (2.7.0.2)
ast (~> 2.4.0)
pg (1.1.4)
pg (1.2.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.1)
puma (4.3.1)
public_suffix (4.0.3)
puma (4.3.3)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
rack (2.0.8)
rack-cache (1.10.0)
rack (2.1.2)
rack-cache (1.11.0)
rack (>= 0.4)
rack-cors (1.1.0)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-protection (2.0.7)
rack-protection (2.0.8.1)
rack
rack-proxy (0.6.5)
rack
@@ -297,7 +337,7 @@ GEM
rainbow (3.0.0)
rake (13.0.1)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.1.3)
redis-namespace (1.7.0)
@@ -307,6 +347,10 @@ GEM
redis-store (>= 1.6, < 2)
redis-store (1.8.1)
redis (>= 4, < 5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
@@ -315,53 +359,65 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
retriable (3.1.2)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta3)
rspec-rails (4.0.0.beta4)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.0)
rubocop (0.78.0)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.2)
rubocop (0.79.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-performance (1.5.1)
rubocop-performance (1.5.2)
rubocop (>= 0.71.0)
rubocop-rails (2.4.0)
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)
ruby-vips (2.0.16)
ffi (~> 1.9)
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)
sentry-raven (2.13.0)
faraday (>= 0.7.6, < 1.0)
shoulda-matchers (4.1.2)
shoulda-matchers (4.2.0)
activesupport (>= 4.2.0)
sidekiq (6.0.4)
connection_pool (>= 2.2.2)
rack (>= 2.0.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
simple_oauth (0.3.1)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simplecov (0.17.1)
docile (~> 1.1)
json (>= 1.8, < 3)
@@ -378,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
@@ -387,27 +443,17 @@ 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.5)
tzinfo (1.2.6)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
unicode-display_width (1.6.1)
uniform_notifier (1.13.0)
valid_email2 (3.1.3)
activemodel (>= 3.2)
@@ -442,13 +488,14 @@ DEPENDENCIES
acts-as-taggable-on
annotate
attr_extras
aws-sdk-s3
azure-storage
bootsnap
brakeman
browser
bullet
bundle-audit
byebug
carrierwave-aws
chargebee
devise
devise_token_auth
@@ -456,10 +503,13 @@ DEPENDENCIES
facebook-messenger
factory_bot_rails
faker
flag_shih_tzu
foreman
google-cloud-storage
haikunator
hashie
jbuilder
json_refs!
jwt
kaminari
koala
@@ -478,21 +528,24 @@ 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
valid_email2
+1 -1
View File
@@ -1,3 +1,3 @@
backend: bin/rails s -p 3000
frontend: bin/webpack-dev-server
worker: bundle exec sidekiq
worker: bundle exec sidekiq -C config/sidekiq.yml
+6 -7
View File
@@ -37,6 +37,11 @@ Detailed documentation is available at [www.chatwoot.com/docs](https://www.chatw
You can find the quick setup docs [here](https://www.chatwoot.com/docs/quick-setup).
## Branching model
We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
## Heroku one-click deploy
Deploying chatwoot to heroku, it's a breeze. It's as simple as clicking this button.
@@ -47,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 = '';
View File
+124 -112
View File
@@ -2,129 +2,141 @@ 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.
module Messages
class MessageBuilder
attr_reader :response
class Messages::MessageBuilder
attr_reader :response
def initialize(response, inbox, outgoing_echo = false)
@response = response
@inbox = inbox
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
@message_type = (outgoing_echo ? :outgoing : :incoming)
def initialize(response, inbox, outgoing_echo = false)
@response = response
@inbox = inbox
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
@message_type = (outgoing_echo ? :outgoing : :incoming)
end
def perform
ActiveRecord::Base.transaction do
build_contact
build_message
end
rescue StandardError => e
Raven.capture_exception(e)
true
end
def perform
ActiveRecord::Base.transaction do
build_contact
build_message
end
rescue StandardError => e
Raven.capture_exception(e)
true
end
private
private
def contact
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
end
def contact
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
end
def build_contact
return if contact.present?
def build_contact
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
@contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
@contact = Contact.create!(contact_params)
ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.new(message_params)
(response.attachments || []).each do |attachment|
@message.build_attachment(attachment_params(attachment))
end
@message.save!
end
def build_attachment; end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params)
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
end
params
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
def location_params(attachment)
lat = attachment['payload']['coordinates']['lat']
long = attachment['payload']['coordinates']['long']
{
external_url: attachment['url'],
coordinates_lat: lat,
coordinates_long: long,
fallback_title: attachment['title']
}
end
def fallback_params(attachment)
{
fallback_title: attachment['title'],
external_url: attachment['url']
}
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id
}
end
def message_params
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
fb_id: response.identifier
}
end
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue Exception => e
result = {}
Raven.capture_exception(e)
end
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || nil
}
def build_message
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
end
def attach_file(attachment, file_url)
file_resource = LocalResource.new(file_url)
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
))
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
end
params
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
def location_params(attachment)
lat = attachment['payload']['coordinates']['lat']
long = attachment['payload']['coordinates']['long']
{
external_url: attachment['url'],
coordinates_lat: lat,
coordinates_long: long,
fallback_title: attachment['title']
}
end
def fallback_params(attachment)
{
fallback_title: attachment['title'],
external_url: attachment['url']
}
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id
}
end
def message_params
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
source_id: response.identifier
}
end
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue Exception => e
result = {}
Raven.capture_exception(e)
end
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || ''
}
end
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
+5
View File
@@ -1,5 +1,10 @@
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from params[:pubsub_token]
::OnlineStatusTracker.add_subscription(params[:pubsub_token])
end
def unsubscribed
::OnlineStatusTracker.remove_subscription(params[:pubsub_token])
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
+1 -1
View File
@@ -4,7 +4,7 @@ class Api::V1::AgentsController < Api::BaseController
before_action :build_agent, only: [:create]
def index
render json: agents
@agents = agents
end
def destroy
+24 -23
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]
@@ -12,8 +11,9 @@ class Api::V1::CallbacksController < ApplicationController
inbox_name = params[:inbox_name]
facebook_channel = current_account.facebook_pages.create!(
name: page_name, page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id)
page_access_token: page_access_token
)
set_avatar(facebook_channel, page_id)
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
render json: inbox
end
@@ -24,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
@@ -45,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
@@ -67,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)
@@ -79,7 +72,16 @@ class Api::V1::CallbacksController < ApplicationController
end
end
def set_avatar(page_id)
def set_avatar(facebook_channel, 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
def get_avatar_url(page_id)
begin
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
uri = URI.parse(url)
@@ -92,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
@@ -0,0 +1,23 @@
class Api::V1::Contacts::ConversationsController < Api::BaseController
def index
@conversations = current_account.conversations.includes(
:assignee, :contact, :inbox
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
end
private
def inbox_ids
if current_user.administrator?
current_account.inboxes.pluck(:id)
elsif current_user.agent?
current_user.assigned_inboxes.pluck(:id)
else
[]
end
end
def permitted_params
params.permit(:contact_id)
end
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
@@ -1,55 +1,45 @@
module Api
module V1
class InboxMembersController < Api::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
class Api::V1::InboxMembersController < Api::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
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
end
def create
# update also done via same action
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
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
end
def show
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
end
private
private
def update_agents_list
# get all the user_ids which the inbox currently has as members.
# get the list of user_ids from params
# the missing ones are the agents which are to be deleted from the inbox
# the new ones are the agents which are to be added to the inbox
def update_agents_list
# get all the user_ids which the inbox currently has as members.
# get the list of user_ids from params
# the missing ones are the agents which are to be deleted from the inbox
# the new ones are the agents which are to be added to the inbox
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
end
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
end
def agents_to_be_added_ids
params[:user_ids] - @current_agents_ids
end
def agents_to_be_added_ids
params[:user_ids] - @current_agents_ids
end
def agents_to_be_removed_ids
@current_agents_ids - params[:user_ids]
end
def agents_to_be_removed_ids
@current_agents_ids - params[:user_ids]
end
def current_agents_ids
@current_agents_ids = @inbox.members.pluck(:id)
end
def current_agents_ids
@current_agents_ids = @inbox.members.pluck(:id)
end
def fetch_inbox
@inbox = current_account.inboxes.find(params[:inbox_id])
end
end
def fetch_inbox
@inbox = current_account.inboxes.find(params[:inbox_id])
end
end
+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
@@ -2,4 +2,8 @@ class Api::V1::LabelsController < Api::BaseController
def index # list all labels in account
@labels = current_account.all_conversation_tags
end
def most_used
@labels = ActsAsTaggableOn::Tag.most_used(params[:count] || 10)
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
+25 -2
View File
@@ -3,7 +3,8 @@ class Api::V1::WebhooksController < ApplicationController
skip_before_action :set_current_user
skip_before_action :check_subscription
before_action :login_from_basic_auth
before_action :login_from_basic_auth, only: [:chargebee]
before_action :check_billing_enabled, only: [:chargebee]
def chargebee
chargebee_consumer.consume
head :ok
@@ -12,8 +13,26 @@ class Api::V1::WebhooksController < ApplicationController
head :ok
end
def twitter_crc
render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
end
def twitter_events
twitter_consumer.consume
head :ok
rescue StandardError => e
Raven.capture_exception(e)
head :ok
end
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']
@@ -21,6 +40,10 @@ class Api::V1::WebhooksController < ApplicationController
end
def chargebee_consumer
@consumer ||= ::Webhooks::Chargebee.new(params)
@chargebee_consumer ||= ::Webhooks::Chargebee.new(params)
end
def twitter_consumer
@twitter_consumer ||= ::Webhooks::Twitter.new(params)
end
end
@@ -0,0 +1,29 @@
class Api::V1::Widget::BaseController < ApplicationController
private
def conversation
@conversation ||= @contact_inbox.conversations.find_by(
inbox_id: auth_token_params[:inbox_id]
)
end
def auth_token_params
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers[header_name]).decode_token
end
def header_name
'X-Auth-Token'
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@account = @web_widget.account
end
def set_contact
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
source_id: auth_token_params[:source_id]
)
@contact = @contact_inbox.contact
end
end
@@ -0,0 +1,13 @@
class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController
before_action :set_web_widget
def index
@inbox_members = @web_widget.inbox.inbox_members.includes(:user)
end
private
def permitted_params
params.permit(:website_token)
end
end
@@ -1,6 +1,8 @@
class Api::V1::Widget::MessagesController < ActionController::Base
skip_before_action :verify_authenticity_token
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
before_action :set_web_widget
before_action :set_contact
before_action :set_conversation, only: [:create]
before_action :set_message, only: [:update]
def index
@messages = conversation.nil? ? [] : message_finder.perform
@@ -9,17 +11,19 @@ class Api::V1::Widget::MessagesController < ActionController::Base
def create
@message = conversation.messages.new(message_params)
@message.save!
render json: @message
end
def update
@message.update!(input_submitted_email: contact_email)
update_contact(contact_email)
head :no_content
rescue StandardError => e
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
end
private
def conversation
@conversation ||= ::Conversation.find_by(
contact_id: cookie_params[:contact_id],
inbox_id: cookie_params[:inbox_id]
)
end
def set_conversation
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
end
@@ -27,9 +31,10 @@ class Api::V1::Widget::MessagesController < ActionController::Base
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
@@ -37,7 +42,8 @@ class Api::V1::Widget::MessagesController < ActionController::Base
{
account_id: inbox.account_id,
inbox_id: inbox.id,
contact_id: cookie_params[:contact_id],
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: {
browser: browser_params,
referer: permitted_params[:message][:referer_url],
@@ -63,13 +69,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base
end
def inbox
@inbox ||= ::Inbox.find_by(id: cookie_params[:inbox_id])
end
def cookie_params
@cookie_params ||= JWT.decode(
request.headers[header_name], secret_key, true, algorithm: 'HS256'
).first.symbolize_keys
@inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
end
def message_finder_params
@@ -83,15 +83,31 @@ class Api::V1::Widget::MessagesController < ActionController::Base
@message_finder ||= MessageFinder.new(conversation, message_finder_params)
end
def header_name
'X-Auth-Token'
def update_contact(email)
contact_with_email = @account.contacts.find_by(email: email)
if contact_with_email
::ContactMergeAction.new(account: @account, base_contact: contact_with_email, mergee_contact: @contact).perform
else
@contact.update!(
email: email,
name: contact_name
)
end
end
def contact_email
permitted_params[:contact][:email].downcase
end
def contact_name
contact_email.split('@')[0]
end
def permitted_params
params.permit(:before, message: [:content, :referer_url, :timestamp])
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
end
def secret_key
Rails.application.secrets.secret_key_base
def set_message
@message = @web_widget.inbox.messages.find(permitted_params[:id])
end
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
@@ -1,4 +1,4 @@
class ConfirmationsController < Devise::ConfirmationsController
class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
skip_before_action :require_no_authentication, raise: false
skip_before_action :authenticate_user!, raise: false
@@ -1,4 +1,4 @@
class PasswordsController < Devise::PasswordsController
class DeviseOverrides::PasswordsController < Devise::PasswordsController
include AuthHelper
skip_before_action :require_no_authentication, raise: false
@@ -0,0 +1,9 @@
class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsController
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
def render_create_success
render 'devise/auth.json'
end
end
@@ -0,0 +1,10 @@
class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenValidationsController
def validate_token
# @resource will have been set by set_user_by_token concern
if @resource
render 'devise/token.json'
else
render_validate_token_error
end
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
-5
View File
@@ -1,5 +0,0 @@
class SessionsController < ::DeviseTokenAuth::SessionsController
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
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
+24 -45
View File
@@ -4,66 +4,45 @@ class WidgetsController < ActionController::Base
before_action :set_contact
before_action :build_contact
def index; end
private
def set_contact
return if cookie_params[:source_id].nil?
contact_inbox = ::ContactInbox.find_by(
inbox_id: @web_widget.inbox.id,
source_id: cookie_params[:source_id]
)
@contact = contact_inbox ? contact_inbox.contact : nil
end
def set_token
@token = conversation_token
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
end
def set_token
@token = permitted_params[:cw_conversation]
@auth_token_params = if @token.present?
::Widget::TokenService.new(token: @token).decode_token
else
{}
end
end
def set_contact
return if @auth_token_params[:source_id].nil?
contact_inbox = ::ContactInbox.find_by(
inbox_id: @web_widget.inbox.id,
source_id: @auth_token_params[:source_id]
)
@contact = contact_inbox ? contact_inbox.contact : nil
end
def build_contact
return if @contact.present?
contact_inbox = @web_widget.create_contact_inbox
@contact = contact_inbox.contact
payload = {
source_id: contact_inbox.source_id,
contact_id: @contact.id,
inbox_id: @web_widget.inbox.id
}
@token = JWT.encode payload, secret_key, 'HS256'
end
def cookie_params
return @cookie_params if @cookie_params.present?
if conversation_token.present?
begin
@cookie_params = JWT.decode(
conversation_token, secret_key, true, algorithm: 'HS256'
).first.symbolize_keys
rescue StandardError
@cookie_params = {}
end
return @cookie_params
end
{}
end
def conversation_token
permitted_params[:cw_conversation]
payload = { source_id: contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
@token = ::Widget::TokenService.new(payload: payload).generate_token
end
def permitted_params
params.permit(:website_token, :cw_conversation)
end
def secret_key
Rails.application.secrets.secret_key_base
end
end
+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
+10 -13
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
@@ -31,6 +26,7 @@ class ConversationFinder
find_all_conversations
filter_by_status
filter_by_labels if params[:labels]
mine_count, unassigned_count, all_count = set_count_for_all_conversations
@@ -61,8 +57,7 @@ class ConversationFinder
end
def set_assignee_type
@assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]]
# ente budhiparamaya neekam kandit enthu tonunu? ;)
@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
@@ -86,6 +79,10 @@ class ConversationFinder
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
end
def filter_by_labels
@conversations = @conversations.tagged_with(params[:labels], any: true)
end
def set_count_for_all_conversations
[
@conversations.assigned_to(current_user).count,
-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);
},
};
+5
View File
@@ -1,9 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class ContactAPI extends ApiClient {
constructor() {
super('contacts');
}
getConversations(contactId) {
return axios.get(`${this.url}/${contactId}/conversations`);
}
}
export default new ContactAPI();
@@ -0,0 +1,18 @@
/* global axios */
import ApiClient from './ApiClient';
class ConversationApi extends ApiClient {
constructor() {
super('conversations');
}
getLabels(conversationID) {
return axios.get(`${this.url}/${conversationID}/labels`);
}
updateLabels(conversationID, labels) {
return axios.post(`${this.url}/${conversationID}/labels`, { labels });
}
}
export default new ConversationApi();
@@ -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 },
});
}
@@ -0,0 +1,14 @@
import agents from '../contacts';
import ApiClient from '../ApiClient';
describe('#ContactsAPI', () => {
it('creates correct instance', () => {
expect(agents).toBeInstanceOf(ApiClient);
expect(agents).toHaveProperty('get');
expect(agents).toHaveProperty('show');
expect(agents).toHaveProperty('create');
expect(agents).toHaveProperty('update');
expect(agents).toHaveProperty('delete');
expect(agents).toHaveProperty('getConversations');
});
});
@@ -0,0 +1,15 @@
import conversations from '../conversations';
import ApiClient from '../ApiClient';
describe('#ConversationApi', () => {
it('creates correct instance', () => {
expect(conversations).toBeInstanceOf(ApiClient);
expect(conversations).toHaveProperty('get');
expect(conversations).toHaveProperty('show');
expect(conversations).toHaveProperty('create');
expect(conversations).toHaveProperty('update');
expect(conversations).toHaveProperty('delete');
expect(conversations).toHaveProperty('getLabels');
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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 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,62 +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;
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;
}
}
@@ -91,4 +92,12 @@
font-weight: $font-weight-medium;
}
}
&.compact {
padding-left: 0;
.conversation--details {
margin-left: 0;
}
}
}
@@ -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;
}
@@ -21,7 +21,7 @@ table {
tr {
.show-if-hover {
opacity: 0;
transition: all .2s $ease-in-out-cubic;
transition: all 0.2s $ease-in-out-cubic;
}
&:hover {
@@ -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>
@@ -1,3 +0,0 @@
<template>
<span class="spinner small"></span>
</template>
@@ -12,7 +12,7 @@
</template>
<script>
import Spinner from '../Spinner';
import Spinner from 'shared/components/Spinner';
export default {
components: {
@@ -15,9 +15,13 @@
/* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import Spinner from '../Spinner';
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;
+1 -1
View File
@@ -1,6 +1,7 @@
/* eslint no-plusplus: 0 */
/* eslint-env browser */
import Spinner from 'shared/components/Spinner';
import Bar from './widgets/chart/BarChart';
import Code from './Code';
import LoadingState from './widgets/LoadingState';
@@ -8,7 +9,6 @@ import Modal from './Modal';
import ModalHeader from './ModalHeader';
import ReportStatsCard from './widgets/ReportStatsCard';
import SidemenuIcon from './SidemenuIcon';
import Spinner from './Spinner';
import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
@@ -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,13 +4,13 @@
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"
@@ -22,6 +22,18 @@
:style="badgeStyle"
src="~dashboard/assets/images/fb-badge.png"
/>
<div
v-else-if="status === 'online'"
class="source-badge user--online"
:style="statusStyle"
></div>
<img
v-if="badge === 'Channel::TwitterProfile'"
id="badge"
class="source-badge"
:style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png"
/>
</div>
</template>
<script>
@@ -41,6 +53,7 @@ export default {
props: {
src: {
type: String,
default: '',
},
size: {
type: String,
@@ -52,6 +65,15 @@ export default {
},
username: {
type: String,
default: '',
},
status: {
type: String,
default: '',
},
hasBorder: {
type: Boolean,
default: false,
},
},
data() {
@@ -67,6 +89,14 @@ export default {
const badgeSize = `${this.avatarSize / 3}px`;
return { width: badgeSize, height: badgeSize };
},
statusStyle() {
const statusSize = `${this.avatarSize / 4}px`;
return { width: statusSize, height: statusSize };
},
thumbnailClass() {
const classname = this.hasBorder ? 'border' : '';
return `user-thumbnail ${classname}`;
},
},
methods: {
onImgError() {
@@ -78,6 +108,7 @@ export default {
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/foundation-settings';
@import '~dashboard/assets/scss/mixins';
.user-thumbnail-box {
@@ -88,14 +119,29 @@ export default {
border-radius: 50%;
height: 100%;
width: 100%;
box-sizing: border-box;
&.border {
border: 1px solid white;
}
}
.source-badge {
bottom: -$space-micro / 2;
bottom: -$space-micro;
height: $space-slab;
position: absolute;
right: $zero;
width: $space-slab;
}
.user--online {
background: $success-color;
border-radius: 50%;
bottom: $space-micro;
&:after {
content: ' ';
}
}
}
</style>
@@ -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);
},
},
};
@@ -5,6 +5,7 @@
@click="cardClick(chat)"
>
<Thumbnail
v-if="!hideThumbnail"
:src="chat.meta.sender.thumbnail"
:badge="chat.meta.sender.channel"
class="columns"
@@ -15,7 +16,7 @@
<h4 class="conversation--user">
{{ chat.meta.sender.name }}
<span
v-if="isInboxNameVisible"
v-if="!hideInboxName && isInboxNameVisible"
v-tooltip.bottom="inboxName(chat.inbox_id)"
class="label"
>
@@ -45,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: {
@@ -58,6 +59,14 @@ export default {
type: Object,
default: () => {},
},
hideInboxName: {
type: Boolean,
default: false,
},
hideThumbnail: {
type: Boolean,
default: false,
},
},
computed: {
@@ -86,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) {

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