Compare commits

...
183 Commits
Author SHA1 Message Date
Sojan 63b1013b45 Merge branch 'release/2.5.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2022-05-16 16:55:24 +05:30
Pranav Raj SandGitHub b8f25d77bf chore: Enable New Message Button on Contact Panel (#4681)
Fixes: #4653
2022-05-16 16:41:40 +05:30
Sojan JoseandGitHub f620bdec54 chore: Update translations (#4682) 2022-05-16 12:45:45 +05:30
Pranav Raj SandGitHub 45e2cd4903 fix: Render links with target attribute (#4685) 2022-05-16 11:29:05 +05:30
Sojan 2060f270d3 Bump version to 2.5.0 2022-05-13 17:26:26 +05:30
Sojan JoseandGitHub 6535624cd6 chore: Update translations from Crowdin (#4665) 2022-05-13 11:38:49 +05:30
Vishnu NarayananandGitHub 8fbe470bdd feat: Control sidekiq log level via rails LOG_LEVEL env variable (#4675) 2022-05-13 11:34:43 +05:30
Hricha ShandilyandGitHub ab3ee74a86 chore: Update the note of Personal message signature (#4679) 2022-05-13 11:34:09 +05:30
Aswin Dev P.SandGitHub 41b8901432 Fix: Agent Reports counts when they have access to multiple accounts (#4663)
This change restricts the agent report API to fetch agent metrics from the current account.

Fixes: #4660
2022-05-11 14:31:57 +05:30
Tejaswini ChileandGitHub 329d305e92 Fix: Creating contacts for already outgoing/echo messages (#4668)
Check if the Instagram contact is already present in the system before throwing the exception.

Fixes: #4666
2022-05-11 14:29:38 +05:30
Sojan JoseandGitHub 5c161e0fef chore: Split Inbox Settings Page Component (#4664)
Clean up the Inbox settings page component as the logic was growing complex.

- Moved subpages like collaborators and configuration to new components
- Moved Assignment settings to the collaborators tab
- Introduced a new inbox seeder to quickly preview setting pages of all inbox types when in development
2022-05-11 13:33:24 +05:30
Pranav Raj SandGitHub 9cec091716 chore: Remove unused method scriptGenerator (#4671) 2022-05-11 12:05:42 +05:30
Tejaswini ChileandGitHub 9ed1f5d96b Fix: Make version changeable from the environment vars (#4654) 2022-05-10 19:20:55 +05:30
Sojan JoseandGitHub 81d0405473 chore: Ability to update user email via Platform APIs (#4659)
When the platform update API is called with a new user email, Chatwoot will still follow the same behaviour as in the dashboard where the user will have to confirm the new email activation link until the email gets updated on the user record.

In the case of platform APIs, this might not be the ideal behaviour since the original app will already have a flow to update the user emails. Hence we need to confirm the emails without the extra step in this case

fixes: #4510
2022-05-10 00:28:46 +05:30
Tejaswini ChileandGitHub f64cf85ab2 Fix: sentry issue for slack incoming files check (#4656)
Interpreter error for nil. any? added nil. present?

Fixes: https://sentry.io/share/issue/48c10d26490f4bdaab78c82244fcea98/
2022-05-09 23:54:45 +05:30
Tejaswini ChileandGitHub 0eeab8c56c Fix: slack repeated callback event message for attached files (#4610) 2022-05-09 19:18:30 +05:30
Sojan JoseandGitHub 5ce29a7beb chore: Update translations from Crowdin (#4652) 2022-05-09 18:57:05 +05:30
Sojan JoseandGitHub 04dfb034cc chore: Upgrade Exception tracking (#4638)
- Upgrade Sentry Libraries
- Enable provision for account and user info in error tracking
- Add ChatwootExceptionTracker

fixes: #4375
2022-05-09 14:23:19 +05:30
Aswin Dev P.SandGitHub 360b438a55 Fix: Campaign triggers first_response reporting event (#4607)
* Fix: Campaign triggers first_response reporting event

* fix spec failure
2022-05-09 01:34:52 -07:00
5b5a6d89c0 chore: "Channel::TwilioSms" to be unique on account_sid+phone_number (#4188)
"Twilio::IncomingMessageService" searches for the correct "Channel::TwilioSms"
by account_sid+phone_number.  If these values are duplicated then which record it
finds is indeterminate and may alternate between queries.

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2022-05-07 17:57:16 +05:30
Pranav Raj SandGitHub 2e0d43c093 chore: Use different files for widget and dashboard audio alert (#4637) 2022-05-06 20:19:36 +05:30
ef850eda0f feat: Add the SDK method to programatically toggle live chat bubble (#4223)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-05-06 19:30:48 +05:30
Sojan JoseandGitHub 80b8f5f915 chore: Update puma config as per recommendations (#3640)
Ability to configure workers for puma

ref:
https://devcenter.heroku.com/articles/concurrency-and-database-connections#threaded-servers
https://www.speedshop.co/2017/10/12/appserver.html
2022-05-06 16:38:44 +05:30
77a6893203 feat: Open "Switch Account" modal when clicking account name on dashboard (#4613)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-05-06 14:55:38 +05:30
Sojan JoseandGitHub 8d2b719dc1 chore: Sentry issues (#4623)
Fixes various issues reported on sentry

- Twilio channel creation validation errors
- Room Channel error with nil class
- Webhook Uri exception
2022-05-06 14:50:55 +05:30
Muhsin KelothandGitHub 4f9d41962a fix: Pre chat custom fields drag and drop (#4530) 2022-05-06 12:01:34 +05:30
Vishnu NarayananandGitHub 66d366f0ff chore: update newrelic gem to support log forwarding (#4626)
Newrelic gem 8.7 has application log forwarding support and
this is enabled by default. Update the gem to make use of this
feature. Chatwoot have turned this off by default though.

Set `NEW_RELIC_APPLICATION_LOGGING_ENABLED` to enable this feature. Make
sure to disable another log forwarding services you have to prevent
duplicated logs.

ref: https://docs.newrelic.com/docs/release-notes/agent-release-notes/ruby-release-notes/ruby-agent-870
Fixes: #4625
2022-05-05 23:57:55 +05:30
Pranav Raj SandGitHub 832a0f24a0 chore: Update missing packages in yarn.lock (#4627) 2022-05-05 19:29:51 +05:30
Sojan JoseandGitHub c9a8796a36 chore: Sentry Issue fixes (#4618)
Address the following sentry issues

fixes: #4616, #4617
2022-05-05 00:31:11 +05:30
giquieuandGitHub b3c8c83830 fix: unable to send audio messages on Telegram (#4493)
- Changed the lib used to record the audio (videojs-record). 
- Changed the audio recording format to .ogg, this will keep compatibility with sending to channels, Telegram, Whatsapp, Web Widget and API.
- Changed the visualization of recording waves, it is now using bars, the same format used by applications (Whatsapp and Telegram)


Fixes: #4115
2022-05-02 13:14:04 +05:30
Shivam ChaharandGitHub c1cc94e37c Fix: Accept phone number in public contact api (#4580)
This PR makes it possible to pass a phone number to the public contacts API.

Fixes #4023
2022-04-29 20:54:12 +05:30
Vishnu NarayananandGitHub 45099f40f1 fix: ce spec action for PRs from forks (#4587)
CE spec action was failing for PRs from external forks at the checkout stage. This PR modifies the checkout action to use the full repo name and branch.

#4586
2022-04-29 14:38:10 +05:30
Shivam ChaharandGitHub b5e0921d0d fix: sidekiq cron deprecation warnings (#4581)
Updates sidekiq-cron to fix the deprecation warnings

Fixes #4090
2022-04-29 00:43:10 +05:30
Tejaswini ChileandGitHub e5136c2691 Fix: File upload from slack (#4494)
When you upload a file from slack chat it's not appearing in the chatwoot inbox.
This is because Slack updated the webhook event format
2022-04-28 21:09:39 +05:30
Shivam ChaharandGitHub 04194e7247 Fix: attachment name for incoming messages
Use original_filename to get the name of the file attached to the message.

Had to add the message_type method to the WhatsApp/incoming_message_service due to the Assignment Branch Condition linter error for the attach_files method.

Fixes #4183
2022-04-28 21:02:17 +05:30
Aswin Dev P.SandGitHub 8d04894744 fix: Add index in conversation and reporting event (#4577)
Fixes chatwoot/product#422
2022-04-28 16:58:06 +05:30
Tejaswini ChileandGitHub cb38ec3267 chore: Allow Facebook channel to receive standby messages (#4511) 2022-04-28 01:14:03 +05:30
Pranav Raj SandGitHub 8348392d43 fix: Disable automation on tweets (#4571)
Fixes #4565
2022-04-27 20:10:40 +05:30
Aswin Dev P.SandGitHub 95c7a24dd1 fix: Unattendeed conversation count in agent metrics (#4568) 2022-04-27 06:26:35 -07:00
Pranav Raj SandGitHub 4250c40d1d chore: Upgrade rails to 6.1.5.1 (#4570) 2022-04-27 18:33:22 +05:30
3d824855f0 Fix positioning of read ticks on attachment message bubbles (#4545)
* Fix positioning of read ticks

* Review fixes

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2022-04-27 08:12:30 +05:30
Aswin Dev P.SandGitHub 1bf2da180b fix: Update agent metrics query to show data from current account (#4557)
* Update agent metrics query to fetch data from current account
2022-04-26 05:52:13 -07:00
Muhsin KelothandGitHub 3a5e34fccb fix: Check phone_number and email changes are exist before validation (#4551) 2022-04-26 15:13:28 +05:30
Sivin VargheseandGitHub fa031a0e2d feat: Adds support for canned responses in contact messages (#4453) 2022-04-26 09:04:34 +05:30
Aswin Dev P.SandGitHub 676796ddc7 feat: Add reports about live agent load (#4537)
* feat: Add reports about live agent load
2022-04-25 07:34:41 -07:00
899176a793 feat: Add event subscription option to webhooks (#4540)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-04-25 17:44:42 +05:30
Muhsin KelothandGitHub fa51fd1d73 fix: Bypass invalid values in contact patch end point (#4519) 2022-04-25 12:25:38 +05:30
1b3011b27b fix: Add Attachment endpoint to save file against automation rule (#4480)
Co-authored-by: fayazara <fayazara@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-24 12:02:40 +05:30
2acb48bbe0 feat: Add support for conversation attribute automation conditions in message event (#4518)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-21 20:52:23 +05:30
Muhsin KelothandGitHub b082b0e58c fix: Dig params in widget contact end point (#4516) 2022-04-21 20:39:45 +05:30
Sojan bd0ed322cc Merge branch 'hotfix/2.4.1' into develop 2022-04-21 14:19:18 +05:30
Sojan 3584bbb3af Merge branch 'hotfix/2.4.1'
Publish Chatwoot CE docker images / build (push) Waiting to run
2022-04-21 14:19:09 +05:30
Pranav Raj SandGitHub f769471439 fix: Issue with closed all hours (#4521)
fixing the accidentally merged conditions for open all day and closed all day.
2022-04-21 14:14:56 +05:30
Sojan f2815a2c00 Bump version to 2.4.1 2022-04-21 14:10:54 +05:30
Muhsin KelothandGitHub 27ddd77a1b chore: Sanitize html content (#4498) 2022-04-21 11:27:28 +05:30
Sojan JoseandGitHub 2c73df4292 Chore: Provide fixed attachment URLs for Channels (#4507)
Prior to this change, The attachment URL sent from Chatwoot to 3rd party integrations like Whatsapp and Facebook
involved a 301 redirect before the original content is served. This causes intermittent breakages for the sent attachments.

fixes: #3632
ref: https://blog.saeloun.com/2021/09/14/rails-7-adds-expiring-urls-to-active-storage.html
2022-04-20 22:42:13 +05:30
2b2252b66e feat: Add a read indicator for web-widget channel (#4224)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-20 16:03:12 +05:30
Pranav Raj SandGitHub f2f0d466f2 chore: Update Help Center APIs to accomodate locale (#4515) 2022-04-20 16:00:37 +05:30
Muhsin KelothandGitHub 4f3a271355 chore: Add custom attributes in campaign.triggered event (#4463) 2022-04-20 10:49:52 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5b9c4bf7f1 chore(deps): bump async from 2.6.3 to 2.6.4 (#4497)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-19 17:13:17 +05:30
Muhsin KelothandGitHub 26f23a6e21 feat: Custom fields in pre-chat form (#4189) 2022-04-19 12:47:29 +05:30
Tejaswini ChileandGitHub 1ccd29140d Feat: send fb message outside of standard messaging window (#4439) 2022-04-19 12:21:20 +05:30
Sojan d2d838afd1 Merge branch 'release/2.4.0' into develop 2022-04-18 23:25:59 +05:30
Sojan 480eb3043c Merge branch 'release/2.4.0'
Publish Chatwoot CE docker images / build (push) Waiting to run
2022-04-18 23:25:37 +05:30
Sojan 569ae24d8a Bump version to 2.4.0 2022-04-18 23:23:17 +05:30
Sojan JoseandGitHub 615a575bdd fix: Agent typing indicator for website widget (#4495)
When we migrated the pubsub tokens from contact to contact inboxes, we missed out on doing this update for the typing indicator events. Hence the agent typing events weren't visible on the widget side. This change fixes that and removes the necessary column contact pubsub token from the model.

fixes: #4476
2022-04-18 19:05:45 +05:30
Pranav Raj SandGitHub 17fb6b8d55 fix: Update business hour calculation (#4496) 2022-04-18 18:15:20 +05:30
Muhsin KelothandGitHub e010f0c6f0 chore: Sync pre-chat fields after custom attribute destroy (#4456) 2022-04-18 14:06:27 +05:30
Shivam ChaharandGitHub 04e890070d fix: Change report_type to type in report API docs (#4492) 2022-04-18 11:38:22 +05:30
Pranav Raj SandGitHub 0319b78eac fix: Allow users to login even if they have access to more than 15 accounts (#4475) 2022-04-14 20:54:26 +05:30
Vishnu NarayananandGitHub 80e5d6d7a0 feat: add chatwoot_edition variable for CE docker images (#4462)
* chore: add chatwoot_edition variable for CE docker images

* fix cw_edition variable

* chore: update comment

* feat: include cw_edition data in payload to hub

* refactor cw_edition to edition
2022-04-14 17:15:57 +05:30
337a74a10c feat: Add send message, fix issues with message conditions (#4423)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
2022-04-14 13:36:55 +05:30
d4be268cc3 enhancement: Remove clickaway for expanded reply box (#4414)
* enhancement: Remove clickaway for expanded reply box

* Removes unused imports

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2022-04-13 12:04:34 +05:30
Tejaswini ChileandGitHub a6b119d187 Feat: twitter image support (#4429) 2022-04-13 01:29:51 +05:30
Tejaswini ChileandGitHub 923b4637db chore: Automation bug fix (#4442) 2022-04-12 20:23:34 +05:30
b6ad468eb4 fix: Nokogiri bundle update (#4448)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-04-12 11:30:54 +05:30
14503b5fe0 feat: Add missing password validation at signup (#4441)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-12 11:08:12 +05:30
c64e2e3bc5 chore: Report improvements (#4392)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-11 20:57:22 +05:30
Aswin Dev P.SandGitHub 31cdc63e18 fix: Remove IMAP and SMTP email validation (#4435)
* Remove IMAP and SMTP email validation
* Rename imap_email & smtp_email columns to imap_login & smtp_login respectively.
* Use channel email domain if inbound email domain not present
2022-04-11 07:07:20 -07:00
Nithin David ThomasandGitHub 3d164271a8 fix: Yellow color shades are inconsistent (#4391) 2022-04-11 17:27:28 +05:30
Sojan JoseandGitHub 1194188f13 chore: Swagger doc for Conversation meta API (#4394)
fixes: https://github.com/chatwoot/chatwoot/issues/4327
2022-04-11 16:43:09 +05:30
Aswin Dev P.SandGitHub 9b5eb98c59 feat: Support additional authentication mechanisms for SMTP (#4431)
* Support additional authentication mechanisms for SMTP
2022-04-11 03:13:05 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8622740161 chore(deps): bump moment from 2.29.1 to 2.29.2 (#4434)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 15:10:37 +05:30
Muhsin KelothandGitHub fb3ed29c90 chore: Add missing test cases in conversations_controller_spec (#4438) 2022-04-11 14:06:44 +05:30
Aswin Dev P.SandGitHub f1ae09d21b fix: SMTP shouldn't be configurable without IMAP enabled (#4428)
* fix: Configure SMTP only if IMAP is enabled
2022-04-11 00:05:03 -07:00
Pranav Raj SandGitHub b1efcde495 feat: Remove duplicate Switch Component (#4427) 2022-04-08 18:25:08 +05:30
Pranav Raj SandGitHub bc7bcc20b8 Revert "feat: Adds support for draft in conversation reply box (#4205)" (#4425)
This reverts commit 5ea0436051.
2022-04-08 15:52:39 +05:30
Muhsin KelothandGitHub 7e5ec7925c feat: Add phone_number & custom_attributes in create conversation end point (#4421) 2022-04-08 14:53:37 +05:30
727993aa19 feat: Add toggle button component(#4419)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2022-04-08 14:02:26 +05:30
Aswin Dev P.SandGitHub d5536d65f7 feat: Consider business hours while generating the reports (#4330)
* feat: Consider business hours while generating the reports
2022-04-08 00:18:18 -07:00
Tejaswini ChileandGitHub 57359be37e Fix: Find mailbox with cc email (#4372) 2022-04-08 11:20:19 +05:30
Sivin VargheseandGitHub 14e6a5d6b0 fix: Background dark mode colour issue in campaign view (#4416) 2022-04-08 10:54:51 +05:30
5ea0436051 feat: Adds support for draft in conversation reply box (#4205)
* Add draft support

* Fixes issue with draft loading

* Adds draft for private notes

* Use localstorage helper

* .remove instead of .clear

* Remove timestamp

* clearLocalStorageOnLogout

* Fix draft save on refresh

* Remove usage of delete operator

* Adds autosave for draft messages

* Remove setinterval and add debounce

* Removes draft redundancy check

* Adds test cases for debouncer

* Update app/javascript/shared/helpers/specs/TimeHelpers.spec.js

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>

* Update app/javascript/shared/helpers/specs/TimeHelpers.spec.js

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>

* Review fixes

* Fixes issue with debouncer

* FIxes debouncer issue

* Fixes issue with draft empty message

* Removes empty keys from local storage drafts

* Fixes error with empty draft

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2022-04-07 22:16:45 +05:30
Muhsin KelothandGitHub dfb56f6bb8 doc: Swagger for custom attribute APIs (#4382) 2022-04-07 19:01:31 +05:30
d4a046a21e fix: Audio notification would not work without action on the dashboard (#4303)
* fixes: Audio notification would not work without an action on dashboard

* fixes spec

* Minor fixes

* Minor fixes

* Review fixes

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-04-07 10:23:18 +05:30
Fayaz AhmedandGitHub 8482ecc1b1 Feat - Sort inboxes alphabetically (#4383) 2022-04-07 10:01:08 +05:30
Sivin VargheseandGitHub 5b490bc8cc chore: fix scroll bar color in the dark mode (#4404) 2022-04-07 07:14:40 +05:30
Muhsin KelothandGitHub c2afdd5c93 chore: Remove unnecessary fields from notification payload (#4403) 2022-04-07 00:07:53 +05:30
Vishnu NarayananandGitHub 75ce5345a9 feat: add gh action to build Chatwoot CE/foss docker image (#4406)
Github action to build and push chatwoot-ce(foss) edition images. This action will run on merges to master, develop and when tags are created. Corresponding docker tags are as follows.

GitHub branch/tag  -->	docker tag
----
master 	--> latest-ce
develop 	---> develop-ce
v2.3.2 	---> v2.3.2-ce
v* 	---> v*-ce

Fixes #4388
2022-04-06 21:14:04 +05:30
9329f08e14 chore: Fix typo in en localisation file (#4405)
Fix

Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
2022-04-06 20:21:19 +05:30
Pranav Raj SandGitHub 0b03e4b296 feat: Add an option to disable endConversation button (#4352) 2022-04-06 13:54:55 +05:30
Tejaswini ChileandGitHub 821b953ee9 Fix: handling UTF encoded mail (#4384) 2022-04-06 12:36:32 +05:30
Sojan JoseandGitHub 7a8aa4ca2e chore: Add Swagger Doc for Team Members (#4390)
Add swagger documentation for Team Members APIs
2022-04-05 16:05:49 +05:30
2595e774e5 fix: Update validations in automation edit actions (#4370)
Co-authored-by: fayazara <fayazara@gmail.com>
2022-04-05 15:22:56 +05:30
Sojan JoseandGitHub 9a8a0bd865 feat: Ability to customize the online presence duration (#4385) 2022-04-05 15:20:34 +05:30
cd5646b4b9 fix: Skip contact_last_seen_at in empty conversation payload (#4387)
* Fix undefined `last_seen_at` empty conversation

* chore: remove the comment

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2022-04-05 14:00:07 +05:30
Vishnu NarayananandGitHub 76f53d9a11 feat: add gh action to run CE edition tests (#4342)
* feat: add gh action to run CE spec

* setup ruby
* add pg,redis services
* fix failing tests
2022-04-04 19:27:01 +05:30
Muhsin KelothandGitHub 591d186d66 chore: Add agent_last_seen_at in conversation API (#4377) 2022-04-04 18:23:38 +05:30
Jordan BroughandGitHub 73d8073530 chore: Fix test failure in action_cable_listener_spec.rb due to ordering (#4357) 2022-04-04 15:15:45 +05:30
Muhsin KelothandGitHub 1dd0c7249c fix: Unread notification count in multiple accounts (#4373)
Fixes: #4367
2022-04-04 14:34:01 +05:30
Pranav Raj SandGitHub 3509692055 chore: Fix brand name in survey translations (#4360) 2022-04-03 19:22:56 +05:30
Sivin VargheseandGitHub 5d85a21287 fix: Fix login button is missing in Safari (#4332)
Fixes #4328
2022-04-01 21:03:53 +05:30
caee9535f1 feat: Support Dark mode for the widget (#4137)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-04-01 20:59:03 +05:30
Vishnu NarayananandGitHub 3813b3b372 feat: make linux script installation non-interactive (#4355)
This PR changes the LetsEncrypt behaviour to be non-interactive. Earlier, the installation flow was waiting for user input at the SSL cert generation stage. With this change, once the user confirms completes the initial selection, the installation can be completed unattended.
2022-04-01 00:34:50 +05:30
Pranav Raj SandGitHub c397fe1964 fix: Update the URL for POST inbox_members (#4354) 2022-03-31 21:21:25 +05:30
cb23ff53bf fix: Reset Captcha if account signup is an error (#4279)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-31 21:20:37 +05:30
Sivin VargheseandGitHub 3cd1616df6 fix: Fix agent name in Twitter channel private note acting as a link (#4326) 2022-03-31 20:22:52 +05:30
Muhsin KelothandGitHub eff3a50316 fix: Disable showing read messages in unread view (#4324) 2022-03-31 17:35:39 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0477123f92 chore(deps): bump minimist from 1.2.5 to 1.2.6 (#4351)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-31 16:33:14 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c9f821cc5b chore(deps): bump puma from 5.6.2 to 5.6.4 (#4348)
Bumps [puma](https://github.com/puma/puma) from 5.6.2 to 5.6.4.
- [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/v5.6.2...v5.6.4)

---
updated-dependencies:
- dependency-name: puma
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-31 16:02:53 +05:30
baf697f064 design: Changes yellow color palette for better contrast ratio (#4334)
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
2022-03-30 18:53:24 +05:30
Tejaswini ChileandGitHub 5be9380547 Fix: html mail fix with html_body content (#4011) 2022-03-30 18:04:30 +05:30
Muhsin KelothandGitHub 24b20c10ce fix: Referer URL validation (#4309)
Fixes #354
2022-03-30 14:36:22 +05:30
Tejaswini ChileandGitHub bfe6324d9a chore: Webhook event data improvements (#4317) 2022-03-30 11:37:36 +05:30
Tejaswini ChileandGitHub 15fd37b124 Feat: attachments automation (#4266) 2022-03-30 08:08:58 +05:30
3f2ac2042f fix: Fetch conversation attributes after contact reset (#4319)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-03-29 19:51:51 +05:30
Tejaswini ChileandGitHub 3158966241 Feat: automation rule based on contact conditions (#4230) 2022-03-29 17:29:34 +05:30
Fayaz AhmedandGitHub c674393c02 feat: New automation actions (#4033) 2022-03-29 13:27:16 +05:30
Kyle KillionandGitHub cffc984ef6 chore: Add active_storage_purge queue to Sidekiq (#4322)
I found that these jobs were just enqueuing without processing until I added this queue after switching to S3 storage. This fixed that issue.
2022-03-29 11:43:07 +05:30
Aswin Dev P.SandGitHub 5e8fd689c9 feat: Add live agent load report api (#4297)
This change allows the admin user to fetch conversation metrics for an account, agents, and filter conversation metrics for a specific agent.

Fixes #4305
2022-03-29 10:31:52 +05:30
Muhsin KelothandGitHub ccf52a620b feat: Update notifications and unread count in real time (#4261) 2022-03-28 20:01:23 +05:30
Vishnu NarayananandGitHub ec0ea0b1dc chore: Upgrade to next gen docker convenience image (#4310)
* chore: Upgrade to next gen docker convenience image

https://discuss.circleci.com/t/legacy-convenience-image-deprecation/41034/16

* switch to postgres:14.1 and redis:6.2 images
2022-03-28 18:15:38 +05:30
Vishnu NarayananandGitHub 8155024b6a feat: fix logging levels (#4314)
https://ruby-doc.org/stdlib-2.7.0/libdoc/logger/rdoc/Logger.html

Fixes https://github.com/chatwoot/chatwoot/issues/4313
2022-03-28 18:14:30 +05:30
823c0ab6a7 chore: Use Round Robin service for team assignment (#4237)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-28 14:38:07 +05:30
e8bc30e3c6 feat: Add a confirmation note before deleting a note about a contact (#4294)
Co-authored-by: Richardo <richardokusuma.sali@medirecords.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-28 14:19:12 +05:30
Fayaz AhmedandGitHub 30ac8054ba feat: Add an SDK method for opening conversation in popout window (#4232) 2022-03-28 13:31:51 +05:30
Aswin Dev P.SandGitHub 0ba6e772a4 feat: Display how many conversations are considered for the metric calculation (#4273)
* feat: Display how many conversations are considered for the metric calculation
2022-03-28 00:38:23 -07:00
Aswin Dev P.SandGitHub ba0188aefc fix: Report spec failure (#4304) 2022-03-28 12:31:09 +05:30
Sojan JoseandGitHub e0f29b9d81 chore: Add swagger docs for Canned Responses (#4295)
fixes: #3962
2022-03-26 00:43:44 +05:30
Pranav Raj SandGitHub 971755b845 fix: Remove unnecessary dispatchWindowEvent (#4288) 2022-03-25 20:25:07 +05:30
0667d9f016 fix: Avatars get cropped on the widget (#4238)
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-03-25 19:20:53 +05:30
Sivin VargheseandGitHub 48389d69d8 feat: New notification panel for unread notifications (#4061)
Fixes: #4052
2022-03-25 19:02:45 +05:30
Sojan JoseandGitHub ff881fcad9 chore: Support Email should give priority to environment variable (#4270)
Account.support_email should give priority to environment variables over installation config

Fixes: #3304
2022-03-25 16:13:45 +05:30
SagarandGitHub 5da0b0667f Fix: SMTP IMAP configuration from email
This fix should allow emails from the "Email" inbox to show from as "Name <email@example.com>". if SMTP within Inbox is disabled.
2022-03-25 16:05:26 +05:30
Jordan BroughandGitHub a8cfcbc168 chore: Timezone fixes for ReportBuilder and for ReportController spec (#4246)
- ReportBuilder wasn't using the specified time zone for the timestamp in the
"build" method
- The ReportController spec was calling `Time.zone.today.to_time`, but
`Date#to_time` disregards the zone from `Time.zone` and reverts to the system
time zone.
2022-03-25 14:00:27 +05:30
6bfe0f2fc1 feat: Add support for rich editor and allow CCs in email for a new conversation. (#4194)
* feat: Add support for rich editor and allow CCs in email for a new conversation.

* Minor fixes

Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-03-25 11:27:45 +05:30
Jordan BroughandGitHub 131c0a8668 chore: Don't send confirmation email when creating User via platform api (#4272)
The platform api automatically confirms users so we don't need to send this
email.
2022-03-25 00:36:59 +05:30
Sojan JoseandGitHub 8e153d6350 fix: Redis 6 on Heroku breaks ActionCable config (#4269)
Heroku made some SSL/TLS changes with Redis 6, which is breaking the ActionCable configuration.
Hence providing an environment variable configuration `REDIS_OPENSSL_VERIFY_MODE` to fix that.

set the value `none` for this environment variable in your Heroku installations where breakage occurs.

fixes: #2420
2022-03-24 19:25:07 +05:30
Tejaswini ChileandGitHub 60a0709de6 Fix: added validation for custom and additional attribute (#4260) 2022-03-24 15:38:28 +05:30
Jordan BroughandGitHub 14c53b8b54 chore: Use "contain_exactly" in some tests (#4243) 2022-03-24 13:50:04 +05:30
Vishnu NarayananandGitHub 7577c9c888 fix: drop conv and campaign seq on account delete (#4256)
Conversation and campaign sequences persist in the database even after the related account is deleted. This PR adds an after_desttory callback on the account model that will delete the associated sequences.

Fixes: #4252
2022-03-24 13:33:15 +05:30
Jordan BroughandGitHub 0cf970dafd chore: Eliminate deprecation warning in "app_config_controller_spec.rb" (#4258)
Fix the warning:

Using `should` from rspec-expectations' old `:should` syntax without
explicitly enabling the syntax is deprecated. Use the new `:expect` syntax
or explicitly enable `:should` with `config.expect_with(:rspec) { |c| c.syntax = :should }` instead.
2022-03-24 13:31:19 +05:30
Jordan BroughandGitHub c2647a1f27 chore: Use "destroy!" instead of "destroy" when not checking the return value (#4259) 2022-03-24 13:28:25 +05:30
giquieuandGitHub 8b9aea231c feat: support audio player on widget (#4250) 2022-03-24 12:44:34 +05:30
Jordan BroughandGitHub b735135431 chore: Fix "action_mailer.delivery_method" in the test env (#4257)
Fixes: #4247
2022-03-23 20:46:28 +05:30
Sojan 160e8a8ab4 Merge branch 'release/2.3.2' into develop 2022-03-23 19:06:52 +05:30
Sojan 2605f24539 Merge branch 'release/2.3.2' 2022-03-23 19:06:45 +05:30
Sojan dd76dd4204 Bump version to 2.3.2 2022-03-23 19:05:34 +05:30
86b4183bde fix: Add multiple file paste support and fix for bugs (#4066)
- Add multiple files paste support.
- Fixes showing file name in the editor field when we paste the file from finder.
- Fixes showing the image in the advance editor when we paste the image as an attachment from the clipboard.

Fixes: #4036

Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
2022-03-23 18:25:57 +05:30
Jordan BroughandGitHub 4a21633a2b chore: Use bang version of "find_or_create_by!" in Platform UsersController (#4248) 2022-03-23 16:09:10 +05:30
Sojan JoseandGitHub f8c46341f6 chore: Disable invalid contact resolved activity messages (#4253)
Fixes the bug which triggered "Contact resolved conversation" activity messages when snoozed conversations where reopened
2022-03-23 16:08:42 +05:30
Pranav Raj SandGitHub 771cb5b9fc fix: Update scroll event name to fix auto-scroll (#4251)
Fixes #3622
2022-03-23 13:27:50 +05:30
Sojan JoseandGitHub f43018e017 chore: New translations automation.json (Italian) (#4231) 2022-03-22 12:42:10 +05:30
Sojan JoseandGitHub 467f3b9191 chore: Disable fetching new emails after mailbox error (#4176)
- Disabled email fetch job if credentials for the channel isn't working
- notify customers when the email channel isn't working

fixes: https://github.com/chatwoot/chatwoot/issues/4174
2022-03-22 12:14:17 +05:30
Muhsin KelothandGitHub 715400f7ab fix: Disable pre-chat fields for identified users (#4234) 2022-03-22 01:28:22 +05:30
Tejaswini ChileandGitHub 7b4cac746a feat: enhancement to un-send instagram message (#4120) 2022-03-21 17:10:15 +05:30
Sojan JoseandGitHub 6169d172d8 chore: Update translations from Crowdin (#4217) 2022-03-21 13:15:33 +05:30
Tejaswini ChileandGitHub e06a6a7c55 Automation enhancement (#4087) 2022-03-21 13:12:27 +05:30
c608ca37e0 fix: Contact conversations are showing in the chat list (#4078)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-21 11:46:43 +05:30
Pranav Raj SandGitHub 6097f4c122 fix: Update survey locale based on the account (#4207) 2022-03-20 13:07:36 +05:30
Fayaz AhmedandGitHub 8372ae418e System default monospace fonts for code elem (#4222) 2022-03-19 20:56:29 +05:30
Sojan JoseandGitHub 71391d982b chore: filter .xml files for Bandwidth SMS (#4206)
MMS sent from an ATT or Verizon phone number seems to be appending a SMIL file in .xml format for MMS.
2022-03-18 19:00:36 +05:30
b77fad52fb fix: Add missing contact information to fix invalid activity messages (#4195)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
2022-03-18 13:43:02 +05:30
Sojan JoseandGitHub 40f31cb0a3 chore: Update translations (#4208) 2022-03-18 11:24:22 +05:30
7f8a199b29 chore: Update translations (#4099)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-17 20:02:32 +05:30
Muhsin KelothandGitHub 23fe8847d8 feat: Notification action cable event (#4154) 2022-03-17 18:56:36 +05:30
Sojan 430547e026 Merge branch 'hotfix/2.3.1' into develop 2022-03-17 16:55:00 +05:30
Sojan d0702363f4 Merge branch 'hotfix/2.3.1' 2022-03-17 16:54:49 +05:30
Sojan d6d0c79618 Bump version to 2.3.1 2022-03-17 16:53:36 +05:30
Sojan JoseandGitHub 9f8e442333 chore: Improve n+1 queries (#4202)
Fixes the n+1 queries that get triggered while accessing agents endpoint
2022-03-17 15:57:32 +05:30
Pranav Raj SandGitHub 4a3ddad02f fix: Reduce the height of the unread container while campaign is active (#4185) 2022-03-16 14:45:57 +05:30
Tejaswini ChileandGitHub 207a03155e Feat: authenticate direct upload (#4160) 2022-03-16 13:54:18 +05:30
Sojan 796a7805db Merge branch 'release/2.3.0' into develop 2022-03-15 23:04:09 +05:30
1333 changed files with 45682 additions and 9641 deletions
+3 -3
View File
@@ -12,8 +12,8 @@ defaults: &defaults
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: circleci/postgres:alpine
- image: circleci/redis:alpine
- image: cimg/postgres:14.1
- image: cimg/redis:6.2.6
environment:
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
- RAILS_LOG_TO_STDOUT: false
@@ -110,7 +110,7 @@ jobs:
- run:
name: Run backend tests
command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace:
root: ~/tmp
+3
View File
@@ -50,3 +50,6 @@ exclude_patterns:
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'
+8 -1
View File
@@ -32,6 +32,11 @@ REDIS_SENTINELS=
# You can find list of master using "SENTINEL masters" command
REDIS_SENTINEL_MASTER_NAME=
# Redis premium breakage in heroku fix
# enable the following configuration
# ref: https://github.com/chatwoot/chatwoot/issues/2420
# REDIS_OPENSSL_VERIFY_MODE=none
# Postgres Database config variables
POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
@@ -156,13 +161,15 @@ USE_INBOX_AVATAR_FOR_BOT=true
## NewRelic
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
# NEW_RELIC_LICENSE_KEY=
# Set this to true to allow newrelic apm to send logs.
# This is turned off by default.
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
## Datadog
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
# DD_TRACE_AGENT_URL=
## IP look up configuration
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
## works only on accounts with ip look up feature enabled
+2 -2
View File
@@ -29,8 +29,8 @@ module.exports = {
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off']
'import/extensions': ['off'],
'no-console': 'error'
},
settings: {
'import/resolver': {
+62
View File
@@ -0,0 +1,62 @@
# #
# # This action will publish Chatwoot CE docker image.
# # This is set to run against merges to develop, master
# # and when tags are created.
# #
name: Publish Chatwoot CE docker images
on:
push:
branches:
- develop
- master
tags:
- v*
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Set Chatwoot edition
run: |
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
- name: set docker tag
run: |
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
- name: replace docker tag if master
if: github.ref_name == 'master'
run: |
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile
push: true
tags: ${{ env.DOCKER_TAG }}
+73
View File
@@ -0,0 +1,73 @@
# #
# # This action will strip the enterprise folder
# # and run the spec.
# # This is set to run against every PR.
# #
name: Run Chatwoot CE spec
on:
push:
branches:
- develop
- master
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:10.8
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ""
POSTGRES_DB: postgres
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
# tmpfs makes DB faster by using RAM
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0.2 # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: yarn
run: yarn install
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: Create database
run: bundle exec rake db:create
- name: Seed database
run: bundle exec rake db:schema:load
- name: yarn check-files
run: yarn install --check-files
# Run rails tests
- name: Run backend tests
run: |
bundle exec rspec --profile=10 --format documentation
+2
View File
@@ -16,6 +16,8 @@ Metrics/ClassLength:
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
+8 -5
View File
@@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'image_processing'
gem 'image_processing', '~> 1.12.2'
##-- gems for database --#
gem 'groupdate'
@@ -97,14 +97,14 @@ gem 'brakeman'
gem 'ddtrace'
gem 'newrelic_rpm'
gem 'scout_apm'
gem 'sentry-rails'
gem 'sentry-ruby'
gem 'sentry-sidekiq'
gem 'sentry-rails', '~> 5.3'
gem 'sentry-ruby', '~> 5.3'
gem 'sentry-sidekiq', '~> 5.3'
##-- background job processing --##
gem 'sidekiq', '~> 6.4.0'
# We want cron jobs
gem 'sidekiq-cron'
gem 'sidekiq-cron', '~> 1.3'
##-- Push notification service --##
gem 'fcm'
@@ -125,6 +125,9 @@ gem 'procore-sift'
gem 'email_reply_trimmer'
gem 'html2text'
# to calculate working hours
gem 'working_hours'
group :production, :staging do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
+85 -81
View File
@@ -9,63 +9,63 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
actioncable (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionmailbox (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
mail (>= 2.7.1)
actionmailer (6.1.4.7)
actionpack (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionmailer (6.1.5.1)
actionpack (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activesupport (= 6.1.5.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.7)
actionview (= 6.1.4.7)
activesupport (= 6.1.4.7)
actionpack (6.1.5.1)
actionview (= 6.1.5.1)
activesupport (= 6.1.5.1)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.7)
actionpack (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
actiontext (6.1.5.1)
actionpack (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
nokogiri (>= 1.8.5)
actionview (6.1.4.7)
activesupport (= 6.1.4.7)
actionview (6.1.5.1)
activesupport (= 6.1.5.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (6.1.4.7)
activesupport (= 6.1.4.7)
activejob (6.1.5.1)
activesupport (= 6.1.5.1)
globalid (>= 0.3.6)
activemodel (6.1.4.7)
activesupport (= 6.1.4.7)
activerecord (6.1.4.7)
activemodel (= 6.1.4.7)
activesupport (= 6.1.4.7)
activemodel (6.1.5.1)
activesupport (= 6.1.5.1)
activerecord (6.1.5.1)
activemodel (= 6.1.5.1)
activesupport (= 6.1.5.1)
activerecord-import (1.3.0)
activerecord (>= 4.2)
activestorage (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activesupport (= 6.1.4.7)
marcel (~> 1.0.0)
activestorage (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activesupport (= 6.1.5.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.4.7)
activesupport (6.1.5.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -136,7 +136,7 @@ GEM
climate_control (1.0.1)
coderay (1.1.3)
commonmarker (0.23.4)
concurrent-ruby (1.1.9)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
crack (0.4.5)
rexml
@@ -183,7 +183,7 @@ GEM
email_reply_trimmer (0.1.13)
erubi (1.10.0)
erubis (2.7.0)
et-orbi (1.2.6)
et-orbi (1.2.7)
tzinfo
execjs (2.8.1)
facebook-messenger (2.0.1)
@@ -210,8 +210,8 @@ GEM
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
foreman (0.87.2)
fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8)
fugit (1.5.3)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
gapic-common (0.3.4)
google-protobuf (~> 3.12, >= 3.12.2)
@@ -349,7 +349,7 @@ GEM
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.14.0)
loofah (2.17.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@@ -376,16 +376,16 @@ GEM
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
netrc (0.11.0)
newrelic_rpm (8.4.0)
newrelic_rpm (8.7.0)
nio4r (2.5.8)
nokogiri (1.13.3)
nokogiri (1.13.5)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.3-arm64-darwin)
nokogiri (1.13.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.3-x86_64-darwin)
nokogiri (1.13.5-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.3-x86_64-linux)
nokogiri (1.13.5-x86_64-linux)
racc (~> 1.4)
oauth (0.5.8)
orm_adapter (0.5.0)
@@ -403,7 +403,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.6.2)
puma (5.6.4)
nio4r (~> 2.0)
pundit (2.2.0)
activesupport (>= 3.0.0)
@@ -419,31 +419,31 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.6.0)
rails (6.1.4.7)
actioncable (= 6.1.4.7)
actionmailbox (= 6.1.4.7)
actionmailer (= 6.1.4.7)
actionpack (= 6.1.4.7)
actiontext (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activemodel (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
rails (6.1.5.1)
actioncable (= 6.1.5.1)
actionmailbox (= 6.1.5.1)
actionmailer (= 6.1.5.1)
actionpack (= 6.1.5.1)
actiontext (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activemodel (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
bundler (>= 1.15.0)
railties (= 6.1.4.7)
railties (= 6.1.5.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
railties (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
method_source
rake (>= 0.13)
rake (>= 12.2)
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
@@ -533,16 +533,16 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.1.0)
sentry-rails (5.3.0)
railties (>= 5.0)
sentry-ruby-core (~> 5.1.0)
sentry-ruby (5.1.0)
sentry-ruby-core (~> 5.3.0)
sentry-ruby (5.3.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-ruby-core (= 5.1.0)
sentry-ruby-core (5.1.0)
sentry-ruby-core (= 5.3.0)
sentry-ruby-core (5.3.0)
concurrent-ruby
sentry-sidekiq (5.1.0)
sentry-ruby-core (~> 5.1.0)
sentry-sidekiq (5.3.0)
sentry-ruby-core (~> 5.3.0)
sidekiq (>= 3.0)
sexp_processor (4.16.0)
shoulda-matchers (5.1.0)
@@ -551,8 +551,8 @@ GEM
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
sidekiq-cron (1.2.0)
fugit (~> 1.1)
sidekiq-cron (1.4.0)
fugit (~> 1)
sidekiq (>= 4.2.1)
signet (0.16.0)
addressable (~> 2.8)
@@ -636,6 +636,9 @@ GEM
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.0)
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.5.4)
PLATFORMS
@@ -688,7 +691,7 @@ DEPENDENCIES
hairtrigger
hashie
html2text
image_processing
image_processing (~> 1.12.2)
jbuilder
json_refs
json_schemer
@@ -723,12 +726,12 @@ DEPENDENCIES
rubocop-rspec
scout_apm
seed_dump
sentry-rails
sentry-ruby
sentry-sidekiq
sentry-rails (~> 5.3)
sentry-ruby (~> 5.3)
sentry-sidekiq (~> 5.3)
shoulda-matchers
sidekiq (~> 6.4.0)
sidekiq-cron
sidekiq-cron (~> 1.3)
simplecov (= 0.17.1)
slack-ruby-client
spring
@@ -746,9 +749,10 @@ DEPENDENCIES
webpacker (~> 5.x)
webpush
wisper (= 2.0.0)
working_hours
RUBY VERSION
ruby 3.0.2p107
BUNDLED WITH
2.3.8
2.3.10
+4
View File
@@ -32,6 +32,10 @@
"INSTALLATION_ENV": {
"description": "Installation method used for Chatwoot.",
"value": "heroku"
},
"REDIS_OPENSSL_VERIFY_MODE":{
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
"value": "none"
}
},
"formation": {
@@ -1,5 +1,5 @@
class Campaigns::CampaignConversationBuilder
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes]
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes]
def perform
@contact_inbox = ContactInbox.find(@contact_inbox_id)
@@ -21,7 +21,8 @@ class Campaigns::CampaignConversationBuilder
def message_params
ActionController::Parameters.new({
content: @campaign.message
content: @campaign.message,
campaign_id: @campaign.id
})
end
@@ -32,7 +33,8 @@ class Campaigns::CampaignConversationBuilder
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
campaign_id: @campaign.id,
additional_attributes: conversation_additional_attributes
additional_attributes: conversation_additional_attributes,
custom_attributes: custom_attributes || {}
}
end
end
+3 -4
View File
@@ -15,11 +15,10 @@ class ContactBuilder
end
def create_contact_inbox(contact)
::ContactInbox.create!(
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: source_id,
hmac_verified: hmac_verified || false
source_id: source_id
)
end
@@ -70,7 +69,7 @@ class ContactBuilder
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
Rails.logger.info e
Rails.logger.error e
raise e
end
end
@@ -27,9 +27,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
rescue StandardError => e
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
true
end
@@ -43,7 +43,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@@ -128,10 +128,10 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
result = {}
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
# We don't need to capture this error as we don't care about contact params in case of echo messages
Sentry.capture_exception(e) unless @outgoing_echo
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
rescue StandardError => e
result = {}
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
end
process_contact_params_result(result)
end
@@ -24,7 +24,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@inbox.channel.authorization_error!
raise
rescue StandardError => e
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
true
end
+10 -1
View File
@@ -9,6 +9,7 @@ class Messages::MessageBuilder
@user = user
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@@ -64,6 +65,14 @@ class Messages::MessageBuilder
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
end
def automation_rule_id
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
end
def campaign_id
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
end
def message_sender
return if @params[:sender_type] != 'AgentBot'
@@ -82,6 +91,6 @@ class Messages::MessageBuilder
items: @items,
in_reply_to: @in_reply_to,
echo_id: @params[:echo_id]
}.merge(external_created_at)
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id)
end
end
@@ -53,16 +53,7 @@ class Messages::Messenger::MessageBuilder
def fetch_story_link(attachment)
message = attachment.message
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(message.source_id, fields: %w[story from]) || {}
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
result = {}
Sentry.capture_exception(e)
end
result = get_story_object_from_source_id(message.source_id)
story_id = result['story']['mention']['id']
story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender
@@ -70,4 +61,15 @@ class Messages::Messenger::MessageBuilder
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
message.save!
end
def get_story_object_from_source_id(source_id)
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
k.get_object(source_id, fields: %w[story from]) || {}
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
{}
end
end
+40 -57
View File
@@ -1,8 +1,10 @@
class V2::ReportBuilder
include DateRangeHelper
include ReportHelper
attr_reader :account, :params
DEFAULT_GROUP_BY = 'day'.freeze
AGENT_RESULTS_PER_PAGE = 25
def initialize(account, params)
@account = account
@@ -18,8 +20,14 @@ class V2::ReportBuilder
# For backward compatible with old report
def build
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].to_time.to_i }
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
end
else
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
end
end
end
@@ -34,23 +42,16 @@ class V2::ReportBuilder
}
end
private
def scope
case params[:type]
when :account
account
when :inbox
inbox
when :agent
user
when :label
label
when :team
team
def conversation_metrics
if params[:type].equal?(:account)
conversations
else
agent_metrics.sort_by { |hash| hash[:metric][:open] }.reverse
end
end
private
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -68,7 +69,7 @@ class V2::ReportBuilder
end
def get_grouped_values(object_scope)
object_scope.group_by_period(
@grouped_values = object_scope.group_by_period(
params[:group_by] || DEFAULT_GROUP_BY,
:created_at,
default_value: 0,
@@ -78,47 +79,29 @@ class V2::ReportBuilder
)
end
def conversations_count
(get_grouped_values scope.conversations).count
def agent_metrics
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
account_users.each_with_object([]) do |account_user, arr|
@user = account_user.user
arr << {
id: @user.id,
name: @user.name,
email: @user.email,
thumbnail: @user.avatar_url,
availability: account_user.availability_status,
metric: conversations
}
end
end
def incoming_messages_count
(get_grouped_values scope.messages.incoming.unscope(:order)).count
end
def outgoing_messages_count
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
end
def resolutions_count
(get_grouped_values scope.conversations.resolved).count
end
def avg_first_response_time
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
end
def avg_resolution_time
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
end
def avg_resolution_time_summary
avg_rt = scope.reporting_events
.where(name: 'conversation_resolved', created_at: range)
.average(:value)
return 0 if avg_rt.blank?
avg_rt
end
def avg_first_response_time_summary
avg_frt = scope.reporting_events
.where(name: 'first_response', created_at: range)
.average(:value)
return 0 if avg_frt.blank?
avg_frt
def conversations
@open_conversations = scope.conversations.where(account_id: @account.id).open
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
metric = {
open: @open_conversations.count,
unattended: @open_conversations.count - first_response_count
}
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
metric
end
end
+2
View File
@@ -45,6 +45,8 @@ class RoomChannel < ApplicationCable::Channel
end
def current_account
return if current_user.blank?
@current_account ||= if @current_user.is_a? Contact
@current_user.account
else
@@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end
def destroy
@agent_bot.destroy
@agent_bot.destroy!
head :ok
end
@@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end
def destroy
@agent.current_account_user.destroy
@agent.current_account_user.destroy!
head :ok
end
@@ -68,7 +68,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end
def agents
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
end
def validate_limit
@@ -7,13 +7,39 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
end
def create
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
@automation_rule.actions = params[:actions]
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
@automation_rule.save!
process_attachments
@automation_rule
end
def attach_file
file_blob = ActiveStorage::Blob.create_and_upload!(
key: nil,
io: params[:attachment].tempfile,
filename: params[:attachment].original_filename,
content_type: params[:attachment].content_type
)
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
end
def show; end
def update
@automation_rule.update(automation_rules_permit)
ActiveRecord::Base.transaction do
@automation_rule.update!(automation_rules_permit)
@automation_rule.actions = params[:actions] if params[:actions]
@automation_rule.save!
process_attachments
rescue StandardError => e
Rails.logger.error e
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
end
end
def destroy
@@ -28,6 +54,17 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
@automation_rule = new_rule
end
def process_attachments
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
return if actions.blank?
actions.each do |action|
blob_id = action['action_params']
blob = ActiveStorage::Blob.find_by(id: blob_id)
@automation_rule.files.attach(blob)
end
end
private
def automation_rules_permit
@@ -1,32 +1,6 @@
class Api::V1::Accounts::BaseController < Api::BaseController
include SwitchLocale
include EnsureCurrentAccountHelper
before_action :current_account
around_action :switch_locale_using_account_locale
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
end
@@ -15,7 +15,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
set_instagram_id(page_access_token, facebook_channel)
set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e).capture_exception
end
end
@@ -60,7 +60,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
set_instagram_id(access_token, fb_page)
fb_page&.reauthorized!
rescue StandardError => e
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e).capture_exception
end
end
@@ -77,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
koala.exchange_access_token_info(omniauth_token)['access_token']
rescue StandardError => e
Rails.logger.info e
Rails.logger.error e
end
def mark_already_existing_facebook_pages(data)
@@ -11,7 +11,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
end
def destroy
@campaign.destroy
@campaign.destroy!
head :ok
end
@@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
end
def destroy
@canned_response.destroy
@canned_response.destroy!
head :ok
end
@@ -7,7 +7,6 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
build_inbox
setup_webhooks if @twilio_channel.sms?
rescue StandardError => e
Sentry.capture_exception(e)
render_could_not_create_error(e.message)
end
end
@@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
end
def destroy
@note.destroy
@note.destroy!
head :ok
end
@@ -1,10 +1,11 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
include EnsureCurrentAccountHelper
before_action :conversation
private
def conversation
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
authorize @conversation.inbox, :show?
end
end
@@ -0,0 +1,17 @@
class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController
include EnsureCurrentAccountHelper
before_action :current_account
before_action :conversation
def create
return if @conversation.nil? || @current_account.nil?
super
end
private
def conversation
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
end
end
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
end
def destroy
@custom_attribute_definition.destroy
@custom_attribute_definition.destroy!
head :no_content
end
@@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
end
def destroy
@custom_filter.destroy
@custom_filter.destroy!
head :no_content
end
@@ -48,7 +48,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
if @inbox.inbox_type == 'Email'
validate_email_channel(channel_attributes)
@inbox.channel.reauthorized!
end
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
@@ -70,7 +73,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def destroy
@inbox.destroy
@inbox.destroy!
head :ok
end
@@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
end
def destroy
@hook.destroy
@hook.destroy!
head :ok
end
@@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
end
def destroy
@hook.destroy
@hook.destroy!
head :ok
end
@@ -4,6 +4,6 @@ class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseControll
private
def portal
@portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id])
@portal ||= Current.account.kbase_portals.find_by(slug: params[:portal_id])
end
end
@@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
end
def destroy
@category.destroy
@category.destroy!
head :ok
end
@@ -1,10 +1,12 @@
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseController
before_action :fetch_portal, except: [:index, :create]
def index
@portals = Current.account.kbase_portals
end
def show; end
def create
@portal = Current.account.kbase_portals.create!(portal_params)
end
@@ -14,14 +16,18 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
end
def destroy
@portal.destroy
@portal.destroy!
head :ok
end
private
def fetch_portal
@portal = current_account.kbase_portals.find(params[:id])
@portal = Current.account.kbase_portals.find_by(slug: permitted_params[:id])
end
def permitted_params
params.permit(:id)
end
def portal_params
@@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
end
def destroy
@label.destroy
@label.destroy!
head :ok
end
@@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
end
def destroy
@team.destroy
@team.destroy!
head :ok
end
@@ -16,14 +16,14 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
end
def destroy
@webhook.destroy
@webhook.destroy!
head :ok
end
private
def webhook_params
params.require(:webhook).permit(:inbox_id, :url)
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
end
def fetch_webhook
@@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy
notification_subscription.destroy!
head :ok
end
@@ -10,7 +10,7 @@ class Api::V1::WebhooksController < ApplicationController
twitter_consumer.consume
head :ok
rescue StandardError => e
Sentry.capture_exception(e)
ChatwootExceptionTracker.new(e).capture_exception
head :ok
end
@@ -1,5 +1,6 @@
class Api::V1::Widget::BaseController < ApplicationController
include SwitchLocale
include WebsiteTokenHelper
before_action :set_web_widget
before_action :set_contact
@@ -19,25 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController
@conversation ||= conversations.last
end
def auth_token_params
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@current_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
raise ActiveRecord::RecordNotFound unless @contact
Current.contact = @contact
end
def create_conversation
::Conversation.create!(conversation_params)
end
@@ -57,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController
browser: browser_params,
referer: permitted_params[:message][:referer_url],
initiated_at: timestamp_params
}
},
custom_attributes: permitted_params[:custom_attributes].presence || {}
}
end
@@ -70,16 +53,33 @@ class Api::V1::Widget::BaseController < ApplicationController
mergee_contact: @contact
).perform
else
@contact.update!(email: email, name: contact_name)
@contact.update!(email: email)
end
end
def update_contact_phone_number(phone_number)
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
if contact_with_phone_number
@contact = ::ContactMergeAction.new(
account: @current_account,
base_contact: contact_with_phone_number,
mergee_contact: @contact
).perform
else
@contact.update!(phone_number: phone_number)
end
end
def contact_email
permitted_params[:contact][:email].downcase
permitted_params.dig(:contact, :email)&.downcase
end
def contact_name
params[:contact][:name] || contact_email.split('@')[0]
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
end
def contact_phone_number
permitted_params.dig(:contact, :phone_number)
end
def browser_params
@@ -96,10 +96,6 @@ class Api::V1::Widget::BaseController < ApplicationController
{ timestamp: permitted_params[:message][:timestamp] }
end
def permitted_params
params.permit(:website_token)
end
def message_params
{
account_id: conversation.account_id,
@@ -7,12 +7,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
def create
ActiveRecord::Base.transaction do
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
process_update_contact
@conversation = create_conversation
conversation.messages.create(message_params)
end
end
def process_update_contact
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
@contact.update!(name: contact_name) if contact_name.present?
end
def update_last_seen
head :ok && return if conversation.nil?
@@ -45,9 +51,12 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_status
head :not_found && return if conversation.nil?
return head :not_found if conversation.nil?
return head :forbidden unless @web_widget.end_conversation?
unless conversation.resolved?
conversation.status = 'resolved'
conversation.status = :resolved
conversation.save
end
head :ok
@@ -60,6 +69,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def permitted_params
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
message: [:content, :referer_url, :timestamp, :echo_id],
custom_attributes: {})
end
end
@@ -0,0 +1,11 @@
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
include WebsiteTokenHelper
before_action :set_web_widget
before_action :set_contact
def create
return if @contact.nil? || @current_account.nil?
super
end
end
@@ -35,41 +35,55 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
end
def conversations
return head :unprocessable_entity if params[:type].blank?
render json: conversation_metrics
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
def current_summary_params
def common_params
{
type: params[:type].to_sym,
id: params[:id],
since: range[:current][:since],
until: range[:current][:until],
group_by: params[:group_by]
group_by: params[:group_by],
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
}
end
def current_summary_params
common_params.merge({
since: range[:current][:since],
until: range[:current][:until]
})
end
def previous_summary_params
{
type: params[:type].to_sym,
id: params[:id],
since: range[:previous][:since],
until: range[:previous][:until],
group_by: params[:group_by]
}
common_params.merge({
since: range[:previous][:since],
until: range[:previous][:until]
})
end
def report_params
common_params.merge({
metric: params[:metric],
since: params[:since],
until: params[:until],
timezone_offset: params[:timezone_offset]
})
end
def conversation_params
{
metric: params[:metric],
type: params[:type].to_sym,
since: params[:since],
until: params[:until],
id: params[:id],
group_by: params[:group_by],
timezone_offset: params[:timezone_offset]
user_id: params[:user_id],
page: params[:page].presence || 1
}
end
@@ -91,4 +105,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
summary
end
def conversation_metrics
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
end
end
@@ -0,0 +1,28 @@
module EnsureCurrentAccountHelper
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
end
@@ -9,8 +9,7 @@ module RequestExceptionHandler
def handle_with_exception
yield
rescue ActiveRecord::RecordNotFound => e
Sentry.capture_exception(e)
rescue ActiveRecord::RecordNotFound
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action')
@@ -0,0 +1,24 @@
module WebsiteTokenHelper
def auth_token_params
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@current_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
raise ActiveRecord::RecordNotFound unless @contact
Current.contact = @contact
end
def permitted_params
params.permit(:website_token)
end
end
+5 -2
View File
@@ -38,9 +38,12 @@ class DashboardController < ActionController::Base
end
def app_config
{ APP_VERSION: Chatwoot.config[:version],
{
APP_VERSION: Chatwoot.config[:version],
VAPID_PUBLIC_KEY: VapidService.public_key,
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', '') }
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v13.0'
}
end
end
@@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController
end
def destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy!
head :ok
end
@@ -7,9 +7,9 @@ class Platform::Api::V1::UsersController < PlatformController
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.skip_confirmation!
@resource.save!
@resource.confirm
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
end
def login
@@ -21,6 +21,10 @@ class Platform::Api::V1::UsersController < PlatformController
def update
@resource.assign_attributes(user_update_params)
# We are using devise's reconfirmable flow for changing emails
# But in case of platform APIs we don't want user to go through this extra step
@resource.skip_reconfirmation! if user_update_params[:email].present?
@resource.save!
end
@@ -43,6 +43,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
end
def permitted_params
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
end
end
@@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
Rails.logger.info e
Rails.logger.error e
redirect_to twitter_app_redirect_url
end
@@ -17,7 +17,7 @@ class Webhooks::InstagramController < ApplicationController
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
render json: :ok
else
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
head :unprocessable_entity
end
end
@@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base
before_action :ensure_widget_position
before_action :ensure_widget_type
before_action :ensure_widget_style
before_action :ensure_dark_mode
def index
render
@@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base
@widget_style = params[:widget_style] || 'standard'
end
def ensure_dark_mode
@dark_mode = params[:dark_mode] || 'light'
end
def ensure_widget_position
@widget_position = params[:position] || 'left'
end
+15
View File
@@ -0,0 +1,15 @@
class EmailChannelFinder
def initialize(email_object)
@email_object = email_object
end
def perform
channel = nil
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
recipient_mails.each do |email|
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
break if channel.present?
end
channel
end
end
+1 -1
View File
@@ -22,7 +22,7 @@ class MessageFinder
def current_messages
if @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before]).limit(20).reverse
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
else
messages.reorder('created_at desc').limit(20).reverse
end
+6 -2
View File
@@ -14,7 +14,7 @@ module Api::V1::InboxesHelper
Mail.defaults do
retriever_method :imap, { address: channel_data[:imap_address],
port: channel_data[:imap_port],
user_name: channel_data[:imap_email],
user_name: channel_data[:imap_login],
password: channel_data[:imap_password],
enable_ssl: channel_data[:imap_enable_ssl] }
end
@@ -29,8 +29,12 @@ module Api::V1::InboxesHelper
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
set_smtp_encryption(channel_data, smtp)
check_smtp_connection(channel_data, smtp)
end
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login)
def check_smtp_connection(channel_data, smtp)
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
channel_data[:smtp_authentication]&.to_sym || :login)
smtp.finish unless smtp&.nil?
end
+68
View File
@@ -0,0 +1,68 @@
module ReportHelper
private
def scope
case params[:type]
when :account
account
when :inbox
inbox
when :agent
user
when :label
label
when :team
team
end
end
def conversations_count
(get_grouped_values scope.conversations.where(account_id: account.id)).count
end
def incoming_messages_count
(get_grouped_values scope.messages.where(account_id: account.id).incoming.unscope(:order)).count
end
def outgoing_messages_count
(get_grouped_values scope.messages.where(account_id: account.id).outgoing.unscope(:order)).count
end
def resolutions_count
(get_grouped_values scope.conversations.where(account_id: account.id).resolved).count
end
def avg_first_response_time
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response', account_id: account.id))
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
grouped_reporting_events.average(:value)
end
def avg_resolution_time
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved', account_id: account.id))
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
grouped_reporting_events.average(:value)
end
def avg_resolution_time_summary
reporting_events = scope.reporting_events
.where(name: 'conversation_resolved', account_id: account.id, created_at: range)
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
return 0 if avg_rt.blank?
avg_rt
end
def avg_first_response_time_summary
reporting_events = scope.reporting_events
.where(name: 'first_response', account_id: account.id, created_at: range)
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
return 0 if avg_frt.blank?
avg_frt
end
end
+50
View File
@@ -0,0 +1,50 @@
module ReportingEventHelper
def business_hours(inbox, from, to)
return 0 unless inbox.working_hours_enabled?
inbox_working_hours = configure_working_hours(inbox.working_hours)
return 0 if inbox_working_hours.blank?
# Configure working hours
WorkingHours::Config.working_hours = inbox_working_hours
# Configure timezone
WorkingHours::Config.time_zone = inbox.timezone
# Use inbox timezone to change from & to values.
from_in_inbox_timezone = from.in_time_zone(inbox.timezone).to_time
to_in_inbox_timezone = to.in_time_zone(inbox.timezone).to_time
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
end
private
def configure_working_hours(working_hours)
working_hours.each_with_object({}) do |working_hour, object|
object[day(working_hour.day_of_week)] = working_hour_range(working_hour) unless working_hour.closed_all_day?
end
end
def day(day_of_week)
week_days = {
0 => :sun,
1 => :mon,
2 => :tue,
3 => :wed,
4 => :thu,
5 => :fri,
6 => :sat
}
week_days[day_of_week]
end
def working_hour_range(working_hour)
{ format_time(working_hour.open_hour, working_hour.open_minutes) => format_time(working_hour.close_hour, working_hour.close_minutes) }
end
def format_time(hour, minute)
hour = hour < 10 ? "0#{hour}" : hour
minute = minute < 10 ? "0#{minute}" : minute
"#{hour}:#{minute}"
end
end
+34 -23
View File
@@ -1,5 +1,5 @@
<template>
<div id="app" class="app-wrapper app-root">
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<transition name="fade" mode="out-in">
<router-view></router-view>
@@ -11,21 +11,28 @@
<woot-snackbar-box />
<network-notification />
</div>
<loading-state v-else />
</template>
<script>
import { accountIdFromPathname } from './helper/URLHelper';
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import {
registerSubscription,
verifyServiceWorkerExistence,
} from './helper/pushHelper';
export default {
name: 'App',
components: {
AddAccountModal,
LoadingState,
NetworkNotification,
UpdateBanner,
WootSnackbarBox,
@@ -43,13 +50,12 @@ export default {
getAccount: 'accounts/getAccount',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
authUIFlags: 'getAuthUIFlags',
currentAccountId: 'getCurrentAccountId',
}),
hasAccounts() {
return (
this.currentUser &&
this.currentUser.accounts &&
this.currentUser.accounts.length !== 0
);
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
},
},
@@ -58,32 +64,37 @@ export default {
if (!this.hasAccounts) {
this.showAddAccountModal = true;
}
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
registerSubscription();
}
})
);
},
currentAccountId() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
},
mounted() {
this.$store.dispatch('setUser');
this.setLocale(window.chatwootConfig.selectedLocale);
this.initializeAccount();
},
methods: {
setLocale(locale) {
this.$root.$i18n.locale = locale;
},
async initializeAccount() {
const { pathname } = window.location;
const accountId = accountIdFromPathname(pathname);
if (accountId) {
await this.$store.dispatch('accounts/get');
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(accountId);
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
}
await this.$store.dispatch('accounts/get');
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken);
},
},
};
+4 -32
View File
@@ -1,6 +1,4 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
import Cookies from 'js-cookie';
import endPoints from './endPoints';
@@ -61,41 +59,15 @@ export default {
});
return fetchPromise;
},
isLoggedIn() {
const hasAuthCookie = !!Cookies.getJSON('auth_data');
const hasUserCookie = !!Cookies.getJSON('user');
return hasAuthCookie && hasUserCookie;
hasAuthCookie() {
return !!Cookies.getJSON('cw_d_session_info');
},
isAdmin() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user').role === 'administrator';
}
return false;
},
getAuthData() {
if (this.isLoggedIn()) {
return Cookies.getJSON('auth_data');
if (this.hasAuthCookie()) {
return Cookies.getJSON('cw_d_session_info');
}
return false;
},
getPubSubToken() {
if (this.isLoggedIn()) {
const user = Cookies.getJSON('user') || {};
const { pubsub_token: pubsubToken } = user;
return pubsubToken;
}
return null;
},
getCurrentUser() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user');
}
return null;
},
verifyPasswordToken({ confirmationToken }) {
return new Promise((resolve, reject) => {
axios
@@ -9,6 +9,14 @@ class AutomationsAPI extends ApiClient {
clone(automationId) {
return axios.post(`${this.url}/${automationId}/clone`);
}
attachment(file) {
return axios.post(`${this.url}/attach_file`, file, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
export default new AutomationsAPI();
+21 -2
View File
@@ -8,7 +8,15 @@ class ReportsAPI extends ApiClient {
super('reports', { accountScoped: true, apiVersion: 'v2' });
}
getReports(metric, since, until, type = 'account', id, group_by) {
getReports(
metric,
since,
until,
type = 'account',
id,
group_by,
business_hours
) {
return axios.get(`${this.url}`, {
params: {
metric,
@@ -17,12 +25,13 @@ class ReportsAPI extends ApiClient {
type,
id,
group_by,
business_hours,
timezone_offset: getTimeOffset(),
},
});
}
getSummary(since, until, type = 'account', id, group_by) {
getSummary(since, until, type = 'account', id, group_by, business_hours) {
return axios.get(`${this.url}/summary`, {
params: {
since,
@@ -30,6 +39,16 @@ class ReportsAPI extends ApiClient {
type,
id,
group_by,
business_hours,
},
});
}
getConversationMetric(type = 'account', page = 1) {
return axios.get(`${this.url}/conversations`, {
params: {
type,
page,
},
});
}
@@ -97,5 +97,18 @@ describe('#Reports API', () => {
}
);
});
it('#getConversationMetric', () => {
reportsAPI.getConversationMetric('account');
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v2/reports/conversations',
{
params: {
type: 'account',
page: 1,
},
}
);
});
});
});
@@ -26,7 +26,8 @@
code {
border: 0;
font-family: 'Monaco', Verdana;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
font-size: $font-size-mini;
&.hljs {
@@ -55,7 +56,6 @@ code {
padding-right: var(--space-normal);
}
.badge {
border-radius: var(--border-radius-normal);
}
@@ -2,6 +2,10 @@
margin-right: var(--space-small);
}
.margin-bottom-small {
margin-bottom: var(--space-small);
}
.margin-right-smaller {
margin-right: var(--space-smaller);
}
@@ -51,10 +55,6 @@
background-color: var(--white);
}
.text-y-800 {
color: var(--y-800);
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
@@ -62,13 +62,13 @@ $default-button-height: 4.0rem;
}
&.warning {
@include button-style(var(--y-100), var(--y-200), var(--y-900));
@include button-style(var(--y-100), var(--y-200), var(--y-700));
}
}
&.clear {
&.warning {
color: var(--y-800);
color: var(--y-600);
}
&.button--only-icon:hover {
@@ -87,7 +87,7 @@ $default-button-height: 4.0rem;
}
&.warning {
background: var(--y-100);
background: var(--y-50);
}
}
}
@@ -27,6 +27,16 @@
padding: 0 $space-small;
}
.video-js {
background: transparent;
// Override min-height : 50px in foundation
//
max-height: $space-mega * 2.4;
min-height: 4.8rem;
padding: var(--space-normal) 0 0;
resize: none;
}
>textarea {
@include ghost-input();
@include margin(0);
@@ -18,6 +18,13 @@
font-size: $font-size-small;
font-weight: $font-weight-bold;
color: $color-heading;
display: flex;
align-items: center;
}
.info-icon {
color: var(--b-400);
margin-left: var(--space-micro);
}
.metric-wrap {
@@ -71,5 +78,10 @@
font-size: $font-size-default;
color: $color-gray;
}
.business-hours {
margin: $space-normal;
text-align: center;
}
}
}
@@ -25,3 +25,21 @@
align-items: center;
display: flex;
}
.business-hours {
align-items: center;
display: flex;
justify-content: end;
margin-bottom: var(--space-normal);
margin-left: auto;
padding-right: var(--space-normal);
}
.business-hours-text {
font-size: var(--font-size-small);
}
.switch {
margin-bottom: var(--space-zero);
margin-left: var(--space-small);
}
@@ -15,9 +15,9 @@
{{ $t('NETWORK.BUTTON.REFRESH') }}
</woot-button>
<woot-button
variant="clear"
variant="smooth"
size="small"
color-scheme="secondary"
color-scheme="warning"
icon="dismiss-circle"
@click="closeNotification"
>
@@ -7,6 +7,10 @@
<p class="sub-head">
{{ subTitle }}
</p>
<p v-if="note">
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
{{ note }}
</p>
</div>
<div class="medium-6 small-12">
<slot></slot>
@@ -25,6 +29,10 @@ export default {
type: String,
required: true,
},
note: {
type: String,
default: '',
},
},
};
</script>
@@ -46,5 +54,9 @@ export default {
.title--section {
padding-right: var(--space-large);
}
.note {
font-weight: var(--font-weight-bold);
}
}
</style>
@@ -57,3 +57,13 @@ export default {
},
};
</script>
<style lang="scss" scoped>
button:disabled {
opacity: 1;
background-color: var(--w-100);
&:hover {
background-color: var(--w-100);
}
}
</style>
@@ -53,7 +53,7 @@ export default {
computed: {
...mapGetters({
getCurrentUserAvailability: 'getCurrentUserAvailability',
getCurrentAccountId: 'getCurrentAccountId',
currentAccountId: 'getCurrentAccountId',
}),
availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
@@ -63,9 +63,6 @@ export default {
availabilityIndex
];
},
currentAccountId() {
return this.getCurrentAccountId;
},
currentUserAvailability() {
return this.getCurrentUserAvailability;
},
@@ -8,6 +8,7 @@
:active-menu-item="activePrimaryMenu.key"
@toggle-accounts="toggleAccountModal"
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<secondary-sidebar
:account-id="accountId"
@@ -18,6 +19,7 @@
:menu-config="activeSecondaryMenu"
:current-role="currentRole"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</aside>
</template>
@@ -176,6 +178,9 @@ export default {
showAddLabelPopup() {
this.$emit('show-add-label-popup');
},
openNotificationPanel() {
this.$emit('open-notification-panel');
},
},
};
</script>
@@ -3,7 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
const reports = accountId => ({
parentNav: 'reports',
routes: [
'settings_account_reports',
'account_overview_reports',
'conversation_reports',
'csat_reports',
'agent_reports',
'label_reports',
@@ -16,7 +17,14 @@ const reports = accountId => ({
label: 'REPORTS_OVERVIEW',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/overview`),
toStateName: 'settings_account_reports',
toStateName: 'account_overview_reports',
},
{
icon: 'chat',
label: 'REPORTS_CONVERSATION',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
toStateName: 'conversation_reports',
},
{
icon: 'emoji',
@@ -1,14 +1,34 @@
<template>
<div v-if="showShowCurrentAccountContext" class="account-context--group">
<div
v-if="showShowCurrentAccountContext"
class="account-context--group"
@mouseover="setShowSwitch"
@mouseleave="resetShowSwitch"
>
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p class="account-context--name text-ellipsis">
{{ account.name }}
</p>
<transition name="fade">
<div v-if="showSwitchButton" class="account-context--switch-group">
<woot-button
variant="clear"
icon="arrow-swap"
class="cursor-pointer"
@click="$emit('toggle-accounts')"
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div>
</transition>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return { showSwitchButton: false };
},
computed: {
...mapGetters({
account: 'getCurrentAccount',
@@ -18,6 +38,14 @@ export default {
return this.userAccounts.length > 1 && this.account.name;
},
},
methods: {
setShowSwitch() {
this.showSwitchButton = true;
},
resetShowSwitch() {
this.showSwitchButton = false;
},
},
};
</script>
<style scoped lang="scss">
@@ -27,10 +55,49 @@ export default {
font-size: var(--font-size-mini);
padding: var(--space-small);
margin-bottom: var(--space-small);
width: 100%;
position: relative;
&:hover {
background: var(--b-100);
}
.account-context--name {
font-weight: var(--font-weight-medium);
margin-bottom: 0;
}
}
.account-context--switch-group {
--overlay-shadow: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
align-items: center;
background-image: var(--overlay-shadow);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
display: flex;
height: 100%;
justify-content: end;
opacity: 1;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 300ms ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
@@ -1,19 +1,21 @@
<template>
<div class="notifications-link">
<primary-nav-item
name="NOTIFICATIONS"
icon="alert"
:to="`/app/accounts/${accountId}/notifications`"
:count="unreadCount"
/>
<woot-button
class-names="notifications-link--button"
variant="clear"
color-scheme="secondary"
:class="{ 'is-active': isNotificationPanelActive }"
@click="openNotificationPanel"
>
<fluent-icon icon="alert" />
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
</woot-button>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PrimaryNavItem from './PrimaryNavItem';
export default {
components: { PrimaryNavItem },
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -28,8 +30,17 @@ export default {
? `${this.notificationMetadata.unreadCount}`
: '99+';
},
isNotificationPanelActive() {
return this.$route.name === 'notifications_index';
},
},
methods: {
openNotificationPanel() {
if (this.$route.name !== 'notifications_index') {
this.$emit('open-notification-panel');
}
},
},
methods: {},
};
</script>
@@ -37,4 +48,32 @@ export default {
.notifications-link {
margin-bottom: var(--space-small);
}
.badge {
position: absolute;
right: var(--space-minus-smaller);
top: var(--space-minus-smaller);
}
.notifications-link--button {
display: flex;
position: relative;
border-radius: var(--border-radius-large);
border: 1px solid transparent;
color: var(--s-600);
margin: var(--space-small) 0;
&:hover {
background: var(--w-50);
color: var(--s-600);
}
&:focus {
border-color: var(--w-500);
}
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
</style>
@@ -16,7 +16,7 @@
/>
</nav>
<div class="menu vertical user-menu">
<notification-bell />
<notification-bell @open-notification-panel="openNotificationPanel" />
<agent-details @toggle-menu="toggleOptions" />
<options-menu
:show="showOptionsMenu"
@@ -83,6 +83,9 @@ export default {
toggleSupportChatWindow() {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$emit('open-notification-panel');
},
},
};
</script>
@@ -1,6 +1,6 @@
<template>
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
<account-context />
<account-context @toggle-accounts="toggleAccountModal" />
<transition-group name="menu-list" tag="ul" class="menu vertical">
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
@@ -85,14 +85,20 @@ export default {
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
toStateName: 'settings_inbox_new',
newLinkRouteName: 'settings_inbox_new',
children: this.inboxes.map(inbox => ({
id: inbox.id,
label: inbox.name,
truncateLabel: true,
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
type: inbox.channel_type,
phoneNumber: inbox.phone_number,
})),
children: this.inboxes
.map(inbox => ({
id: inbox.id,
label: inbox.name,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/inbox/${inbox.id}`
),
type: inbox.channel_type,
phoneNumber: inbox.phone_number,
}))
.sort((a, b) =>
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
),
};
},
labelSection() {
@@ -218,6 +224,9 @@ export default {
showAddLabelPopup() {
this.$emit('add-label');
},
toggleAccountModal() {
this.$emit('toggle-accounts');
},
},
};
</script>
@@ -14,6 +14,10 @@ const i18nConfig = new VueI18n({
messages: i18n,
});
const $route = {
name: 'notifications_index',
};
describe('notificationBell', () => {
const accountId = 1;
const notificationMetadata = { unreadCount: 19 };
@@ -45,24 +49,40 @@ describe('notificationBell', () => {
});
it('it should return unread count 19 ', () => {
const notificationBell = shallowMount(NotificationBell, {
store,
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
expect(statusViewTitle.vm.count).toBe('19');
expect(wrapper.vm.unreadCount).toBe('19');
});
it('it should return unread count 99+ ', async () => {
notificationMetadata.unreadCount = 101;
notificationMetadata.unreadCount = 100;
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
expect(wrapper.vm.unreadCount).toBe('99+');
});
it('isNotificationPanelActive', async () => {
const notificationBell = shallowMount(NotificationBell, {
store,
localVue,
i18n: i18nConfig,
mocks: {
$route,
},
});
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
expect(statusViewTitle.vm.count).toBe('99+');
expect(notificationBell.vm.isNotificationPanelActive).toBe(true);
});
});
@@ -112,10 +112,10 @@ export default {
}
&.warning {
background: var(--y-800);
color: var(--s-600);
background: var(--y-600);
color: var(--y-500);
a {
color: var(--s-600);
color: var(--y-500);
}
}
@@ -157,7 +157,7 @@ export default {
&.warning {
background: var(--y-100);
color: var(--y-900);
border: 1px solid var(--y-300);
border: 1px solid var(--y-200);
a {
color: var(--y-900);
}
@@ -1,54 +1,66 @@
<template>
<label class="switch" :class="classObject">
<input
:id="id"
v-model="value"
class="switch-input"
:name="name"
:disabled="disabled"
type="checkbox"
/>
<div class="switch-paddle" :for="name">
<span class="show-for-sr">on off</span>
</div>
</label>
<button
type="button"
class="toggle-button"
:class="{ active: value }"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
>
<span aria-hidden="true" :class="{ active: value }"></span>
</button>
</template>
<script>
export default {
props: {
disabled: Boolean,
type: { type: String, default: '' },
size: { type: String, default: '' },
checked: Boolean,
name: { type: String, default: '' },
id: { type: String, default: '' },
value: { type: Boolean, default: false },
},
data() {
return {
value: null,
};
},
computed: {
classObject() {
const { type, size, value } = this;
return {
[`is-${type}`]: type,
[`${size}`]: size,
checked: value,
};
methods: {
onClick() {
this.$emit('input', !this.value);
},
},
watch: {
value(val) {
this.$emit('input', val);
},
},
beforeMount() {
this.value = this.checked;
},
mounted() {
this.$emit('input', (this.value = !!this.checked));
},
};
</script>
<style lang="scss" scoped>
.toggle-button {
--toggle-button-box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
background-color: var(--s-200);
border-radius: var(--border-radius-large);
border: 2px solid transparent;
cursor: pointer;
display: flex;
flex-shrink: 0;
height: 19px;
position: relative;
transition-duration: 200ms;
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
width: 34px;
&.active {
background-color: var(--w-500);
}
span {
--space-one-point-five: 1.5rem;
background-color: var(--white);
border-radius: 100%;
box-shadow: var(--toggle-button-box-shadow);
display: inline-block;
height: var(--space-one-point-five);
transform: translate(0, 0);
transition-duration: 200ms;
transition-property: transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
width: var(--space-one-point-five);
&.active {
transform: translate(var(--space-one-point-five), var(--space-zero));
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div>
<div class="preview-item__wrap">
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
@@ -19,7 +19,7 @@
</span>
</div>
<div class="file-size-wrap">
<span class="item">
<span class="item text-truncate">
{{ formatFileSize(attachment.resource) }}
</span>
</div>
@@ -70,15 +70,23 @@ export default {
};
</script>
<style lang="scss" scoped>
.preview-item__wrap {
display: flex;
flex-direction: column;
overflow: auto;
margin-top: var(--space-normal);
max-height: 20rem;
}
.preview-item {
display: flex;
padding: var(--space-slab) 0 0;
background: var(--color-background-light);
background: var(--b-50);
border-radius: var(--border-radius-normal);
width: fit-content;
width: 24rem;
padding: var(--space-smaller);
margin-top: var(--space-normal);
margin-bottom: var(--space-one);
}
.thumb-wrap {
@@ -114,6 +122,7 @@ export default {
> .item {
margin: 0;
overflow: hidden;
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
}
@@ -124,7 +133,8 @@ export default {
}
.file-name-wrap {
max-width: 50%;
max-width: 60%;
min-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: var(--space-small);
@@ -7,7 +7,8 @@
<select
v-model="action_name"
class="action__question"
@change="resetFilter()"
:class="{ 'full-width': !showActionInput }"
@change="resetAction()"
>
<option
v-for="attribute in actionTypes"
@@ -17,20 +18,44 @@
{{ attribute.label }}
</option>
</select>
<div class="filter__answer--wrap">
<div class="multiselect-wrap--small">
<multiselect
<div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType">
<div
v-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="'Select'"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
/>
</div>
<input
v-else-if="inputType === 'email'"
v-model="action_params"
track-by="id"
label="name"
:placeholder="'Select'"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
type="email"
class="answer--text-input"
placeholder="Enter email"
/>
<input
v-else-if="inputType === 'url'"
v-model="action_params"
type="url"
class="answer--text-input"
placeholder="Enter url"
/>
<automation-action-file-input
v-if="inputType === 'attachment'"
v-model="action_params"
:initial-file-name="initialFileName"
/>
</div>
</div>
@@ -41,6 +66,18 @@
@click="removeAction"
/>
</div>
<automation-action-team-message-input
v-if="inputType === 'team_message'"
v-model="action_params"
:teams="dropdownValues"
/>
<textarea
v-if="inputType === 'textarea'"
v-model="action_params"
rows="4"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
class="action-message"
></textarea>
<p
v-if="v.action_params.$dirty && v.action_params.$error"
class="filter-error"
@@ -51,7 +88,13 @@
</template>
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
export default {
components: {
AutomationActionTeamMessageInput,
AutomationActionFileInput,
},
props: {
value: {
type: Object,
@@ -69,6 +112,14 @@ export default {
type: Object,
default: () => null,
},
showActionInput: {
type: Boolean,
default: true,
},
initialFileName: {
type: String,
default: '',
},
},
computed: {
action_name: {
@@ -91,13 +142,17 @@ export default {
this.$emit('input', { ...payload, action_params: value });
},
},
inputType() {
return this.actionTypes.find(action => action.key === this.action_name)
.inputType;
},
},
methods: {
removeAction() {
this.$emit('removeAction');
},
resetFilter() {
this.$emit('resetFilter');
resetAction() {
this.$emit('resetAction');
},
},
};
@@ -136,9 +191,14 @@ export default {
max-width: 50%;
}
.action__question.full-width {
max-width: 100%;
}
.filter__answer--wrap {
margin-right: var(--space-smaller);
flex-grow: 1;
max-width: 50%;
input {
margin-bottom: 0;
@@ -179,4 +239,7 @@ export default {
.multiselect {
margin-bottom: var(--space-zero);
}
.action-message {
margin: var(--space-small) 0 0;
}
</style>
@@ -0,0 +1,62 @@
<template>
<div>
<div class="multiselect-wrap--small">
<multiselect
v-model="selectedTeams"
track-by="id"
label="name"
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="teams"
:allow-empty="false"
@input="updateValue"
/>
<textarea
v-model="message"
rows="4"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
@input="updateValue"
></textarea>
</div>
</div>
</template>
<script>
export default {
// The value types are dynamic, hence prop validation removed to work with our action schema
// eslint-disable-next-line vue/require-prop-types
props: ['teams', 'value'],
data() {
return {
selectedTeams: [],
message: '',
};
},
mounted() {
const { team_ids: teamIds } = this.value;
this.selectedTeams = teamIds;
this.message = this.value.message;
},
methods: {
updateValue() {
this.$emit('input', {
team_ids: this.selectedTeams.map(team => team.id),
message: this.message,
});
},
},
};
</script>
<style scoped>
.multiselect {
margin: var(--space-smaller) var(--space-zero);
}
textarea {
margin-bottom: var(--space-zero);
}
</style>
@@ -0,0 +1,117 @@
<template>
<label class="input-wrapper" :class="uploadState">
<input
v-if="uploadState !== 'processing'"
type="file"
name="attachment"
:class="uploadState === 'processing' ? 'disabled' : ''"
@change="onChangeFile"
/>
<spinner v-if="uploadState === 'processing'" />
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
<fluent-icon
v-if="uploadState === 'uploaded'"
icon="checkmark-circle"
type="outline"
class="success-icon"
/>
<fluent-icon
v-if="uploadState === 'failed'"
icon="dismiss-circle"
type="outline"
class="error-icon"
/>
<p class="file-button">{{ label }}</p>
</label>
</template>
<script>
import Spinner from 'shared/components/Spinner';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Spinner,
},
mixins: [alertMixin],
props: {
value: {
type: Array,
default: () => [],
},
initialFileName: {
type: String,
default: '',
},
},
data() {
return {
uploadState: 'idle',
label: this.$t('AUTOMATION.ATTACHMENT.LABEL_IDLE'),
};
},
mounted() {
if (this.initialFileName) {
this.label = this.initialFileName;
this.uploadState = 'uploaded';
}
},
methods: {
async onChangeFile(event) {
this.uploadState = 'processing';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
try {
const file = event.target.files[0];
const formData = new FormData();
formData.append('attachment', file, file.name);
const id = await this.$store.dispatch(
'automations/uploadAttachment',
formData
);
this.$emit('input', [id]);
this.uploadState = 'uploaded';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADED');
} catch (error) {
this.uploadState = 'failed';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
}
},
},
};
</script>
<style scoped>
input[type='file'] {
display: none;
}
.input-wrapper {
display: flex;
height: 39px;
background-color: var(--white);
border-radius: var(--border-radius-small);
border: 1px dashed var(--w-100);
padding: var(--space-smaller) var(--space-small);
align-items: center;
font-size: var(--font-size-mini);
cursor: pointer;
}
.success-icon {
margin-right: var(--space-small);
color: var(--g-500);
}
.error-icon {
margin-right: var(--space-small);
color: var(--r-500);
}
.processing {
cursor: not-allowed;
opacity: 0.9;
}
.file-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
</style>
@@ -5,7 +5,14 @@
@click="onClick(index)"
>
<h3 class="heading">
{{ heading }}
<span>{{ heading }}</span>
<fluent-icon
v-if="infoText"
v-tooltip="infoText"
size="14"
icon="info"
class="info-icon"
/>
</h3>
<div class="metric-wrap">
<h4 class="metric">
@@ -22,6 +29,7 @@
export default {
props: {
heading: { type: String, default: '' },
infoText: { type: String, default: '' },
point: { type: [Number, String], default: '' },
trend: { type: Number, default: null },
index: { type: Number, default: null },
@@ -0,0 +1,50 @@
<template>
<span>
{{ textToBeDisplayed }}
<button class="show-more--button" @click="toggleShowMore">
{{ buttonLabel }}
</button>
</span>
</template>
<script>
export default {
props: {
text: {
type: String,
default: '',
},
limit: {
type: Number,
default: 120,
},
},
data() {
return {
showMore: false,
};
},
computed: {
textToBeDisplayed() {
if (this.showMore) {
return this.text;
}
return this.text.slice(0, this.limit) + '...';
},
buttonLabel() {
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
},
},
methods: {
toggleShowMore() {
this.showMore = !this.showMore;
},
},
};
</script>
<style scoped>
.show-more--button {
color: var(--w-500);
}
</style>
@@ -209,7 +209,7 @@ export default {
}
.user-online-status--busy {
background: var(--y-700);
background: var(--y-500);
}
.user-online-status--offline {
@@ -1,16 +1,29 @@
<template>
<div class="audio-wave-wrapper">
<div id="audio-wave"></div>
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin"></audio>
</div>
</template>
<script>
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
import RecordRTC from 'recordrtc';
import 'video.js/dist/video-js.css';
import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import inboxMixin from '../../../../shared/mixins/inboxMixin';
import alertMixin from '../../../../shared/mixins/alertMixin';
import Recorder from 'opus-recorder';
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-record/dist/videojs.record.js';
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import { format, addSeconds } from 'date-fns';
WaveSurfer.microphone = MicrophonePlugin;
export default {
@@ -18,104 +31,116 @@ export default {
mixins: [inboxMixin, alertMixin],
data() {
return {
wavesurfer: false,
recorder: false,
recordingInterval: false,
recordingDateStarted: new Date().getTime(),
timeDuration: '00:00',
player: false,
recordingDateStarted: new Date(0),
initialTimeDuration: '00:00',
options: {
container: '#audio-wave',
backend: 'WebAudio',
interact: true,
cursorWidth: 1,
plugins: [
WaveSurfer.microphone.create({
bufferSize: 4096,
numberOfInputChannels: 1,
numberOfOutputChannels: 1,
constraints: {
video: false,
audio: true,
},
}),
],
},
optionsRecorder: {
type: 'audio',
mimeType: 'audio/wav',
disableLogs: true,
recorderType: RecordRTC.StereoAudioRecorder,
sampleRate: 44100,
numberOfAudioChannels: 2,
checkForInactiveTracks: true,
bufferSize: 4096,
recorderOptions: {
debug: true,
controls: true,
bigPlayButton: false,
fluid: false,
controlBar: {
deviceButton: false,
fullscreenToggle: false,
cameraButton: false,
volumePanel: false,
},
plugins: {
wavesurfer: {
backend: 'WebAudio',
waveColor: '#1f93ff',
progressColor: 'rgb(25, 118, 204)',
cursorColor: 'rgba(43, 51, 63, 0.7)',
backgroundColor: 'none',
barWidth: 1,
cursorWidth: 1,
hideScrollbar: true,
plugins: [
WaveSurfer.microphone.create({
bufferSize: 4096,
numberOfInputChannels: 1,
numberOfOutputChannels: 1,
constraints: {
video: false,
audio: true,
},
}),
],
},
record: {
audio: true,
video: false,
displayMilliseconds: false,
maxLength: 300,
audioEngine: 'opus-recorder',
audioWorkerURL: encoderWorker,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
},
},
},
};
},
computed: {
isRecording() {
if (this.recorder) {
return this.recorder.getState() === 'recording';
}
return false;
return this.player && this.player.record().isRecording();
},
},
mounted() {
this.wavesurfer = WaveSurfer.create(this.options);
this.wavesurfer.on('play', this.playingRecorder);
this.wavesurfer.on('pause', this.pausedRecorder);
this.wavesurfer.microphone.on('deviceReady', this.startRecording);
this.wavesurfer.microphone.on('deviceError', this.deviceError);
this.wavesurfer.microphone.start();
this.fireStateRecorderTimerChanged(this.initialTimeDuration);
window.Recorder = Recorder;
this.fireProgressRecord(this.initialTimeDuration);
this.player = videojs('#audio-wave', this.recorderOptions, () => {
this.$nextTick(() => {
this.player.record().getDevice();
});
});
this.player.on('deviceReady', this.deviceReady);
this.player.on('deviceError', this.deviceError);
this.player.on('startRecord', this.startRecord);
this.player.on('stopRecord', this.stopRecord);
this.player.on('progressRecord', this.progressRecord);
this.player.on('finishRecord', this.finishRecord);
this.player.on('playbackFinish', this.playbackFinish);
},
beforeDestroy() {
if (this.recorder) {
this.recorder.destroy();
if (this.player) {
this.player.dispose();
}
if (this.wavesurfer) {
this.wavesurfer.destroy();
if (window.Recorder) {
window.Recorder = undefined;
}
},
methods: {
startRecording(stream) {
this.recorder = RecordRTC(stream, this.optionsRecorder);
this.recorder.onStateChanged = this.onStateRecorderChanged;
this.recorder.startRecording();
deviceReady() {
this.player.record().start();
},
startRecord() {
this.fireStateRecorderChanged('recording');
},
stopRecord() {
this.fireStateRecorderChanged('stopped');
},
finishRecord() {
const file = new File(
[this.player.recordedData],
this.player.recordedData.name,
{ type: this.player.recordedData.type }
);
this.fireRecorderBlob(file);
},
progressRecord() {
this.fireProgressRecord(this.formatTimeProgress());
},
stopAudioRecording() {
if (this.isRecording) {
this.recorder.stopRecording(() => {
this.wavesurfer.microphone.stopDevice();
this.wavesurfer.loadBlob(this.recorder.getBlob());
this.wavesurfer.stop();
this.fireRecorderBlob(this.getAudioFile());
});
}
this.player.record().stop();
},
getAudioFile() {
if (this.hasAudio()) {
return new File([this.recorder.getBlob()], this.getAudioFileName(), {
type: 'audio/wav',
});
}
return false;
},
hasAudio() {
return !(this.isRecording || this.wavesurfer.isPlaying());
},
playingRecorder() {
this.fireStateRecorderChanged('playing');
},
pausedRecorder() {
this.fireStateRecorderChanged('paused');
},
deviceError(err) {
deviceError() {
const deviceError = this.player.deviceErrorCode;
const deviceErrorName = deviceError?.name.toLowerCase();
if (
err?.name &&
(err.name.toLowerCase().includes('notallowederror') ||
err.name.toLowerCase().includes('permissiondeniederror'))
deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror')
) {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
@@ -127,56 +152,37 @@ export default {
);
}
},
onStateRecorderChanged(state) {
// recording stopped inactive destroyed
switch (state) {
case 'recording':
this.timerDurationChanged();
break;
case 'stopped':
this.timerDurationChanged();
break;
default:
break;
}
this.fireStateRecorderChanged(state);
},
timerDurationChanged() {
if (this.isRecording) {
this.recordingInterval = setInterval(() => {
this.calculateTimeDuration(
(new Date().getTime() - this.recordingDateStarted) / 1000
);
this.fireStateRecorderTimerChanged(this.timeDuration);
}, 1000);
} else {
clearInterval(this.recordingInterval);
}
},
calculateTimeDuration(secs) {
let hr = Math.floor(secs / 3600);
let min = Math.floor((secs - hr * 3600) / 60);
let sec = Math.floor(secs - hr * 3600 - min * 60);
if (min < 10) {
min = '0' + min;
}
if (sec < 10) {
sec = '0' + sec;
}
if (hr <= 0) {
this.timeDuration = min + ':' + sec;
} else {
if (hr < 10) {
hr = '0' + hr;
}
this.timeDuration = hr + ':' + min + ':' + sec;
}
formatTimeProgress() {
return format(
addSeconds(
new Date(this.recordingDateStarted.getTimezoneOffset() * 1000 * 60),
this.player.record().getDuration()
),
'mm:ss'
);
},
playPause() {
this.wavesurfer.playPause();
if (this.player.wavesurfer().surfer.isPlaying()) {
this.fireStateRecorderChanged('paused');
} else {
this.fireStateRecorderChanged('playing');
}
this.player.wavesurfer().surfer.playPause();
},
play() {
this.fireStateRecorderChanged('playing');
this.player.wavesurfer().play();
},
pause() {
this.fireStateRecorderChanged('paused');
this.player.wavesurfer().pause();
},
playbackFinish() {
this.fireStateRecorderChanged('paused');
this.player.wavesurfer().pause();
},
fireRecorderBlob(blob) {
this.$emit('recorder-blob', {
this.$emit('finish-record', {
name: blob.name,
type: blob.type,
size: blob.size,
@@ -186,29 +192,8 @@ export default {
fireStateRecorderChanged(state) {
this.$emit('state-recorder-changed', state);
},
fireStateRecorderTimerChanged(duration) {
this.$emit('state-recorder-timer-changed', duration);
},
getAudioFileName() {
const d = new Date();
return `audio-${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${this.getRandomString()}.wav`;
},
getRandomString() {
if (
window.crypto &&
window.crypto.getRandomValues &&
navigator.userAgent.indexOf('Safari') === -1
) {
let a = window.crypto.getRandomValues(new Uint32Array(3));
let token = '';
for (let i = 0, l = a.length; i < l; i += 1) {
token += a[i].toString(36);
}
return token.toLowerCase();
}
return (Math.random() * new Date().getTime())
.toString(36)
.replace(/\./g, '');
fireProgressRecord(duration) {
this.$emit('state-recorder-progress-changed', duration);
},
},
};
@@ -217,7 +202,9 @@ export default {
<style lang="scss">
.audio-wave-wrapper {
min-height: 8rem;
max-height: 12rem;
overflow: hidden;
height: 8rem;
}
.video-js .vjs-control-bar {
background-color: transparent;
}
</style>
@@ -186,6 +186,12 @@ export default {
blur: () => {
this.onBlur();
},
paste: (view, event) => {
const data = event.clipboardData.files;
if (data.length > 0) {
event.preventDefault();
}
},
},
});
this.focusEditorInputField();
@@ -163,13 +163,13 @@ export default {
&:hover,
&:active {
color: var(--y-800);
color: var(--y-700);
}
}
}
.button--note {
color: var(--y-900);
color: var(--y-600);
}
.action-wrap {
@@ -3,7 +3,7 @@ import { Bar } from 'vue-chartjs';
const fontFamily =
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
const chartOptions = {
const defaultChartOptions = {
responsive: true,
maintainAspectRatio: false,
legend: {
@@ -11,10 +11,14 @@ const chartOptions = {
fontFamily,
},
},
datasets: {
bar: {
barPercentage: 1.0,
},
},
scales: {
xAxes: [
{
barPercentage: 1.1,
ticks: {
fontFamily,
},
@@ -39,8 +43,20 @@ const chartOptions = {
export default {
extends: Bar,
props: ['collection'],
props: {
collection: {
type: Object,
default: () => {},
},
chartOptions: {
type: Object,
default: () => {},
},
},
mounted() {
this.renderChart(this.collection, chartOptions);
this.renderChart(this.collection, {
...defaultChartOptions,
...this.chartOptions,
});
},
};
@@ -23,7 +23,7 @@ export default {
background: var(--s-500);
}
&__busy {
background: var(--y-700);
background: var(--y-500);
}
}
</style>
@@ -14,8 +14,8 @@
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
class="text-y-800"
size="14"
class="hmac-warning__icon"
icon="warning"
/>
</h3>
@@ -181,7 +181,11 @@ export default {
.snoozed--display-text {
font-weight: var(--font-weight-medium);
color: var(--y-900);
color: var(--y-600);
}
}
.hmac-warning__icon {
color: var(--y-600);
}
</style>
@@ -60,6 +60,7 @@
:readable-time="readableTime"
:source-id="data.source_id"
:inbox-id="data.inbox_id"
:message-read="showReadTicks"
/>
</div>
<spinner v-if="isPending" size="tiny" />
@@ -153,6 +154,14 @@ export default {
type: Boolean,
default: false,
},
hasUserReadMessage: {
type: Boolean,
default: false,
},
isWebWidgetInbox: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -207,7 +216,11 @@ export default {
}
}
return (
this.formatMessage(this.data.content, this.isATweet) + botMessageContent
this.formatMessage(
this.data.content,
this.isATweet,
this.data.private
) + botMessageContent
);
},
contentAttributes() {
@@ -264,6 +277,14 @@ export default {
isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
},
showReadTicks() {
return (
(this.isOutgoing || this.isTemplate) &&
this.hasUserReadMessage &&
this.isWebWidgetInbox &&
!this.data.private
);
},
isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
},

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