Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
1eccf79cf7
30 changed files with 424 additions and 349 deletions
3
.github/workflows/run_foss_spec.yml
vendored
3
.github/workflows/run_foss_spec.yml
vendored
|
@ -42,7 +42,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -104,7 +104,7 @@ gem 'sentry-sidekiq'
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq', '~> 6.4.0'
|
gem 'sidekiq', '~> 6.4.0'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron'
|
gem 'sidekiq-cron', '~> 1.3'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
|
|
128
Gemfile.lock
128
Gemfile.lock
|
@ -9,63 +9,63 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.4.7)
|
actioncable (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.4.7)
|
actionmailbox (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.4.7)
|
actionmailer (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.4.7)
|
actionpack (6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.4.7)
|
actiontext (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.4.7)
|
actionview (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.4.7)
|
activejob (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.4.7)
|
activemodel (6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
activerecord (6.1.4.7)
|
activerecord (6.1.5.1)
|
||||||
activemodel (= 6.1.4.7)
|
activemodel (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
activerecord-import (1.3.0)
|
activerecord-import (1.3.0)
|
||||||
activerecord (>= 4.2)
|
activerecord (>= 4.2)
|
||||||
activestorage (6.1.4.7)
|
activestorage (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.4.7)
|
activesupport (6.1.5.1)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
@ -136,7 +136,7 @@ GEM
|
||||||
climate_control (1.0.1)
|
climate_control (1.0.1)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.4)
|
commonmarker (0.23.4)
|
||||||
concurrent-ruby (1.1.9)
|
concurrent-ruby (1.1.10)
|
||||||
connection_pool (2.2.5)
|
connection_pool (2.2.5)
|
||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
|
@ -183,7 +183,7 @@ GEM
|
||||||
email_reply_trimmer (0.1.13)
|
email_reply_trimmer (0.1.13)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
et-orbi (1.2.6)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.8.1)
|
execjs (2.8.1)
|
||||||
facebook-messenger (2.0.1)
|
facebook-messenger (2.0.1)
|
||||||
|
@ -210,8 +210,8 @@ GEM
|
||||||
ruby_parser (~> 3.0)
|
ruby_parser (~> 3.0)
|
||||||
sexp_processor (~> 4.0)
|
sexp_processor (~> 4.0)
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
fugit (1.5.2)
|
fugit (1.5.3)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
gapic-common (0.3.4)
|
gapic-common (0.3.4)
|
||||||
google-protobuf (~> 3.12, >= 3.12.2)
|
google-protobuf (~> 3.12, >= 3.12.2)
|
||||||
|
@ -349,7 +349,7 @@ GEM
|
||||||
listen (3.7.1)
|
listen (3.7.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.14.0)
|
loofah (2.16.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -419,31 +419,31 @@ GEM
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-timeout (0.6.0)
|
rack-timeout (0.6.0)
|
||||||
rails (6.1.4.7)
|
rails (6.1.5.1)
|
||||||
actioncable (= 6.1.4.7)
|
actioncable (= 6.1.5.1)
|
||||||
actionmailbox (= 6.1.4.7)
|
actionmailbox (= 6.1.5.1)
|
||||||
actionmailer (= 6.1.4.7)
|
actionmailer (= 6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
actiontext (= 6.1.4.7)
|
actiontext (= 6.1.5.1)
|
||||||
actionview (= 6.1.4.7)
|
actionview (= 6.1.5.1)
|
||||||
activejob (= 6.1.4.7)
|
activejob (= 6.1.5.1)
|
||||||
activemodel (= 6.1.4.7)
|
activemodel (= 6.1.5.1)
|
||||||
activerecord (= 6.1.4.7)
|
activerecord (= 6.1.5.1)
|
||||||
activestorage (= 6.1.4.7)
|
activestorage (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.4.7)
|
railties (= 6.1.5.1)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.2)
|
rails-html-sanitizer (1.4.2)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.1.4.7)
|
railties (6.1.5.1)
|
||||||
actionpack (= 6.1.4.7)
|
actionpack (= 6.1.5.1)
|
||||||
activesupport (= 6.1.4.7)
|
activesupport (= 6.1.5.1)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
|
@ -551,8 +551,8 @@ GEM
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
sidekiq-cron (1.2.0)
|
sidekiq-cron (1.4.0)
|
||||||
fugit (~> 1.1)
|
fugit (~> 1)
|
||||||
sidekiq (>= 4.2.1)
|
sidekiq (>= 4.2.1)
|
||||||
signet (0.16.0)
|
signet (0.16.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
|
@ -731,7 +731,7 @@ DEPENDENCIES
|
||||||
sentry-sidekiq
|
sentry-sidekiq
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (~> 6.4.0)
|
sidekiq (~> 6.4.0)
|
||||||
sidekiq-cron
|
sidekiq-cron (~> 1.3)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client
|
slack-ruby-client
|
||||||
spring
|
spring
|
||||||
|
@ -755,4 +755,4 @@ RUBY VERSION
|
||||||
ruby 3.0.2p107
|
ruby 3.0.2p107
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.8
|
2.3.9
|
||||||
|
|
|
@ -96,7 +96,7 @@ class V2::ReportBuilder
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||||
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||||
metric = {
|
metric = {
|
||||||
open: @open_conversations.count,
|
open: @open_conversations.count,
|
||||||
unattended: @open_conversations.count - first_response_count
|
unattended: @open_conversations.count - first_response_count
|
||||||
|
|
|
@ -43,6 +43,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,16 @@
|
||||||
padding: 0 $space-small;
|
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 {
|
>textarea {
|
||||||
@include ghost-input();
|
@include ghost-input();
|
||||||
@include margin(0);
|
@include margin(0);
|
||||||
|
|
|
@ -1,16 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="audio-wave-wrapper">
|
<div class="audio-wave-wrapper">
|
||||||
<div id="audio-wave"></div>
|
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin"></audio>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
import 'video.js/dist/video-js.css';
|
||||||
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
import 'videojs-record/dist/css/videojs.record.css';
|
||||||
import RecordRTC from 'recordrtc';
|
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
||||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
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;
|
WaveSurfer.microphone = MicrophonePlugin;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -18,104 +31,116 @@ export default {
|
||||||
mixins: [inboxMixin, alertMixin],
|
mixins: [inboxMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
wavesurfer: false,
|
player: false,
|
||||||
recorder: false,
|
recordingDateStarted: new Date(0),
|
||||||
recordingInterval: false,
|
|
||||||
recordingDateStarted: new Date().getTime(),
|
|
||||||
timeDuration: '00:00',
|
|
||||||
initialTimeDuration: '00:00',
|
initialTimeDuration: '00:00',
|
||||||
options: {
|
recorderOptions: {
|
||||||
container: '#audio-wave',
|
debug: true,
|
||||||
backend: 'WebAudio',
|
controls: true,
|
||||||
interact: true,
|
bigPlayButton: false,
|
||||||
cursorWidth: 1,
|
fluid: false,
|
||||||
plugins: [
|
controlBar: {
|
||||||
WaveSurfer.microphone.create({
|
deviceButton: false,
|
||||||
bufferSize: 4096,
|
fullscreenToggle: false,
|
||||||
numberOfInputChannels: 1,
|
cameraButton: false,
|
||||||
numberOfOutputChannels: 1,
|
volumePanel: false,
|
||||||
constraints: {
|
},
|
||||||
video: false,
|
plugins: {
|
||||||
audio: true,
|
wavesurfer: {
|
||||||
},
|
backend: 'WebAudio',
|
||||||
}),
|
waveColor: '#1f93ff',
|
||||||
],
|
progressColor: 'rgb(25, 118, 204)',
|
||||||
},
|
cursorColor: 'rgba(43, 51, 63, 0.7)',
|
||||||
optionsRecorder: {
|
backgroundColor: 'none',
|
||||||
type: 'audio',
|
barWidth: 1,
|
||||||
mimeType: 'audio/wav',
|
cursorWidth: 1,
|
||||||
disableLogs: true,
|
hideScrollbar: true,
|
||||||
recorderType: RecordRTC.StereoAudioRecorder,
|
plugins: [
|
||||||
sampleRate: 44100,
|
WaveSurfer.microphone.create({
|
||||||
numberOfAudioChannels: 2,
|
bufferSize: 4096,
|
||||||
checkForInactiveTracks: true,
|
numberOfInputChannels: 1,
|
||||||
bufferSize: 4096,
|
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: {
|
computed: {
|
||||||
isRecording() {
|
isRecording() {
|
||||||
if (this.recorder) {
|
return this.player && this.player.record().isRecording();
|
||||||
return this.recorder.getState() === 'recording';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.wavesurfer = WaveSurfer.create(this.options);
|
window.Recorder = Recorder;
|
||||||
this.wavesurfer.on('play', this.playingRecorder);
|
this.fireProgressRecord(this.initialTimeDuration);
|
||||||
this.wavesurfer.on('pause', this.pausedRecorder);
|
this.player = videojs('#audio-wave', this.recorderOptions, () => {
|
||||||
this.wavesurfer.microphone.on('deviceReady', this.startRecording);
|
this.$nextTick(() => {
|
||||||
this.wavesurfer.microphone.on('deviceError', this.deviceError);
|
this.player.record().getDevice();
|
||||||
this.wavesurfer.microphone.start();
|
});
|
||||||
this.fireStateRecorderTimerChanged(this.initialTimeDuration);
|
});
|
||||||
|
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() {
|
beforeDestroy() {
|
||||||
if (this.recorder) {
|
if (this.player) {
|
||||||
this.recorder.destroy();
|
this.player.dispose();
|
||||||
}
|
}
|
||||||
if (this.wavesurfer) {
|
if (window.Recorder) {
|
||||||
this.wavesurfer.destroy();
|
window.Recorder = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startRecording(stream) {
|
deviceReady() {
|
||||||
this.recorder = RecordRTC(stream, this.optionsRecorder);
|
this.player.record().start();
|
||||||
this.recorder.onStateChanged = this.onStateRecorderChanged;
|
},
|
||||||
this.recorder.startRecording();
|
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() {
|
stopAudioRecording() {
|
||||||
if (this.isRecording) {
|
this.player.record().stop();
|
||||||
this.recorder.stopRecording(() => {
|
|
||||||
this.wavesurfer.microphone.stopDevice();
|
|
||||||
this.wavesurfer.loadBlob(this.recorder.getBlob());
|
|
||||||
this.wavesurfer.stop();
|
|
||||||
this.fireRecorderBlob(this.getAudioFile());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getAudioFile() {
|
deviceError() {
|
||||||
if (this.hasAudio()) {
|
const deviceError = this.player.deviceErrorCode;
|
||||||
return new File([this.recorder.getBlob()], this.getAudioFileName(), {
|
const deviceErrorName = deviceError?.name.toLowerCase();
|
||||||
type: 'audio/wav',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
hasAudio() {
|
|
||||||
return !(this.isRecording || this.wavesurfer.isPlaying());
|
|
||||||
},
|
|
||||||
playingRecorder() {
|
|
||||||
this.fireStateRecorderChanged('playing');
|
|
||||||
},
|
|
||||||
pausedRecorder() {
|
|
||||||
this.fireStateRecorderChanged('paused');
|
|
||||||
},
|
|
||||||
deviceError(err) {
|
|
||||||
if (
|
if (
|
||||||
err?.name &&
|
deviceErrorName?.includes('notallowederror') ||
|
||||||
(err.name.toLowerCase().includes('notallowederror') ||
|
deviceErrorName?.includes('permissiondeniederror')
|
||||||
err.name.toLowerCase().includes('permissiondeniederror'))
|
|
||||||
) {
|
) {
|
||||||
this.showAlert(
|
this.showAlert(
|
||||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
||||||
|
@ -127,56 +152,37 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateRecorderChanged(state) {
|
formatTimeProgress() {
|
||||||
// recording stopped inactive destroyed
|
return format(
|
||||||
switch (state) {
|
addSeconds(
|
||||||
case 'recording':
|
new Date(this.recordingDateStarted.getTimezoneOffset() * 1000 * 60),
|
||||||
this.timerDurationChanged();
|
this.player.record().getDuration()
|
||||||
break;
|
),
|
||||||
case 'stopped':
|
'mm:ss'
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
playPause() {
|
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) {
|
fireRecorderBlob(blob) {
|
||||||
this.$emit('recorder-blob', {
|
this.$emit('finish-record', {
|
||||||
name: blob.name,
|
name: blob.name,
|
||||||
type: blob.type,
|
type: blob.type,
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
|
@ -186,29 +192,8 @@ export default {
|
||||||
fireStateRecorderChanged(state) {
|
fireStateRecorderChanged(state) {
|
||||||
this.$emit('state-recorder-changed', state);
|
this.$emit('state-recorder-changed', state);
|
||||||
},
|
},
|
||||||
fireStateRecorderTimerChanged(duration) {
|
fireProgressRecord(duration) {
|
||||||
this.$emit('state-recorder-timer-changed', duration);
|
this.$emit('state-recorder-progress-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, '');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -217,7 +202,9 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.audio-wave-wrapper {
|
.audio-wave-wrapper {
|
||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
max-height: 12rem;
|
height: 8rem;
|
||||||
overflow: hidden;
|
}
|
||||||
|
.video-js .vjs-control-bar {
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,9 +36,9 @@
|
||||||
<woot-audio-recorder
|
<woot-audio-recorder
|
||||||
v-if="showAudioRecorderEditor"
|
v-if="showAudioRecorderEditor"
|
||||||
ref="audioRecorderInput"
|
ref="audioRecorderInput"
|
||||||
@state-recorder-timer-changed="onStateRecorderTimerChanged"
|
@state-recorder-progress-changed="onStateProgressRecorderChanged"
|
||||||
@state-recorder-changed="onStateRecorderChanged"
|
@state-recorder-changed="onStateRecorderChanged"
|
||||||
@recorder-blob="onRecorderBlob"
|
@finish-record="onFinishRecorder"
|
||||||
/>
|
/>
|
||||||
<resizable-text-area
|
<resizable-text-area
|
||||||
v-else-if="!showRichContentEditor"
|
v-else-if="!showRichContentEditor"
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
:show-emoji-picker="showEmojiPicker"
|
:show-emoji-picker="showEmojiPicker"
|
||||||
:on-send="sendMessage"
|
:on-send="sendMessage"
|
||||||
:is-send-disabled="isReplyButtonDisabled"
|
:is-send-disabled="isReplyButtonDisabled"
|
||||||
:recording-audio-duration-text="recordingAudioDuration"
|
:recording-audio-duration-text="recordingAudioDurationText"
|
||||||
:recording-audio-state="recordingAudioState"
|
:recording-audio-state="recordingAudioState"
|
||||||
:is-recording-audio="isRecordingAudio"
|
:is-recording-audio="isRecordingAudio"
|
||||||
:set-format-mode="setFormatMode"
|
:set-format-mode="setFormatMode"
|
||||||
|
@ -193,7 +193,7 @@ export default {
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
isRecordingAudio: false,
|
isRecordingAudio: false,
|
||||||
recordingAudioState: '',
|
recordingAudioState: '',
|
||||||
recordingAudioDuration: '',
|
recordingAudioDurationText: '',
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
|
@ -585,11 +585,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleAudioRecorderPlayPause() {
|
toggleAudioRecorderPlayPause() {
|
||||||
if (this.isRecordingAudio && !this.isRecorderAudioStopped) {
|
if (this.isRecordingAudio) {
|
||||||
this.isRecorderAudioStopped = true;
|
if (!this.isRecorderAudioStopped) {
|
||||||
this.$refs.audioRecorderInput.stopAudioRecording();
|
this.isRecorderAudioStopped = true;
|
||||||
} else if (this.isRecordingAudio && this.isRecorderAudioStopped) {
|
this.$refs.audioRecorderInput.stopAudioRecording();
|
||||||
this.$refs.audioRecorderInput.playPause();
|
} else if (this.isRecorderAudioStopped) {
|
||||||
|
this.$refs.audioRecorderInput.playPause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideEmojiPicker() {
|
hideEmojiPicker() {
|
||||||
|
@ -612,19 +614,17 @@ export default {
|
||||||
onFocus() {
|
onFocus() {
|
||||||
this.isFocused = true;
|
this.isFocused = true;
|
||||||
},
|
},
|
||||||
onStateRecorderTimerChanged(time) {
|
onStateProgressRecorderChanged(duration) {
|
||||||
this.recordingAudioDuration = time;
|
this.recordingAudioDurationText = duration;
|
||||||
},
|
},
|
||||||
onStateRecorderChanged(state) {
|
onStateRecorderChanged(state) {
|
||||||
this.recordingAudioState = state;
|
this.recordingAudioState = state;
|
||||||
if (state.includes('notallowederror')) {
|
if (state && 'notallowederror'.includes(state)) {
|
||||||
this.toggleAudioRecorder();
|
this.toggleAudioRecorder();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRecorderBlob(file) {
|
onFinishRecorder(file) {
|
||||||
if (file) {
|
return file && this.onFileUpload(file);
|
||||||
this.onFileUpload(file);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
toggleTyping(status) {
|
toggleTyping(status) {
|
||||||
const conversationId = this.currentChat.id;
|
const conversationId = this.currentChat.id;
|
||||||
|
|
|
@ -91,9 +91,9 @@
|
||||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||||
"microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z",
|
"microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z",
|
||||||
"microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z",
|
"microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z",
|
||||||
"microphone-stop-outline": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9",
|
"microphone-stop-outline": "M18,18H6V6H18V18Z",
|
||||||
"microphone-pause-outline": "M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
|
"microphone-pause-outline": "M14,19H18V5H14M6,19H10V5H6V19Z",
|
||||||
"microphone-play-outline": "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z",
|
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
|
||||||
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
||||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||||
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
|
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
response = Facebook::Messenger::Subscriptions.subscribe(
|
response = Facebook::Messenger::Subscriptions.subscribe(
|
||||||
access_token: page_access_token,
|
access_token: page_access_token,
|
||||||
subscribed_fields: %w[
|
subscribed_fields: %w[
|
||||||
messages message_deliveries message_echoes message_reads
|
messages message_deliveries message_echoes message_reads standby messaging_handovers
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|
|
@ -90,6 +90,8 @@ class Channel::Telegram < ApplicationRecord
|
||||||
telegram_attachment = {}
|
telegram_attachment = {}
|
||||||
|
|
||||||
case attachment[:file_type]
|
case attachment[:file_type]
|
||||||
|
when 'audio'
|
||||||
|
telegram_attachment[:type] = 'audio'
|
||||||
when 'image'
|
when 'image'
|
||||||
telegram_attachment[:type] = 'photo'
|
telegram_attachment[:type] = 'photo'
|
||||||
when 'file'
|
when 'file'
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id)
|
# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id)
|
||||||
# index_conversations_on_campaign_id (campaign_id)
|
# index_conversations_on_campaign_id (campaign_id)
|
||||||
# index_conversations_on_contact_inbox_id (contact_inbox_id)
|
# index_conversations_on_contact_inbox_id (contact_inbox_id)
|
||||||
|
# index_conversations_on_last_activity_at (last_activity_at)
|
||||||
# index_conversations_on_status_and_account_id (status,account_id)
|
# index_conversations_on_status_and_account_id (status,account_id)
|
||||||
# index_conversations_on_team_id (team_id)
|
# index_conversations_on_team_id (team_id)
|
||||||
#
|
#
|
||||||
|
|
|
@ -17,11 +17,12 @@
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_reporting_events_on_account_id (account_id)
|
# index_reporting_events_on_account_id (account_id)
|
||||||
# index_reporting_events_on_created_at (created_at)
|
# index_reporting_events_on_conversation_id (conversation_id)
|
||||||
# index_reporting_events_on_inbox_id (inbox_id)
|
# index_reporting_events_on_created_at (created_at)
|
||||||
# index_reporting_events_on_name (name)
|
# index_reporting_events_on_inbox_id (inbox_id)
|
||||||
# index_reporting_events_on_user_id (user_id)
|
# index_reporting_events_on_name (name)
|
||||||
|
# index_reporting_events_on_user_id (user_id)
|
||||||
#
|
#
|
||||||
|
|
||||||
class ReportingEvent < ApplicationRecord
|
class ReportingEvent < ApplicationRecord
|
||||||
|
|
|
@ -22,6 +22,8 @@ class AutomationRules::ActionService
|
||||||
private
|
private
|
||||||
|
|
||||||
def send_attachment(blob_ids)
|
def send_attachment(blob_ids)
|
||||||
|
return if conversation_a_tweet?
|
||||||
|
|
||||||
return unless @rule.files.attached?
|
return unless @rule.files.attached?
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.find(blob_ids)
|
blob = ActiveStorage::Blob.find(blob_ids)
|
||||||
|
@ -61,6 +63,8 @@ class AutomationRules::ActionService
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_message(message)
|
def send_message(message)
|
||||||
|
return if conversation_a_tweet?
|
||||||
|
|
||||||
params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } }
|
params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } }
|
||||||
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
mb = Messages::MessageBuilder.new(nil, @conversation, params)
|
||||||
mb.perform
|
mb.perform
|
||||||
|
@ -101,4 +105,10 @@ class AutomationRules::ActionService
|
||||||
def team_belongs_to_account?(team_ids)
|
def team_belongs_to_account?(team_ids)
|
||||||
@account.team_ids.include?(team_ids[0])
|
@account.team_ids.include?(team_ids[0])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_a_tweet?
|
||||||
|
return false if @conversation.additional_attributes.blank?
|
||||||
|
|
||||||
|
@conversation.additional_attributes['type'] == 'tweet'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Sms::IncomingMessageService
|
||||||
file_type: file_type(attachment_file.content_type),
|
file_type: file_type(attachment_file.content_type),
|
||||||
file: {
|
file: {
|
||||||
io: attachment_file,
|
io: attachment_file,
|
||||||
filename: attachment_file,
|
filename: attachment_file.original_filename,
|
||||||
content_type: attachment_file.content_type
|
content_type: attachment_file.content_type
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -105,7 +105,7 @@ class Telegram::IncomingMessageService
|
||||||
file_type: file_content_type,
|
file_type: file_content_type,
|
||||||
file: {
|
file: {
|
||||||
io: attachment_file,
|
io: attachment_file,
|
||||||
filename: attachment_file,
|
filename: attachment_file.original_filename,
|
||||||
content_type: attachment_file.content_type
|
content_type: attachment_file.content_type
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -77,7 +77,6 @@ class Whatsapp::IncomingMessageService
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_files
|
def attach_files
|
||||||
message_type = params[:messages].first[:type]
|
|
||||||
return if %w[text button interactive].include?(message_type)
|
return if %w[text button interactive].include?(message_type)
|
||||||
|
|
||||||
attachment_payload = params[:messages].first[message_type.to_sym]
|
attachment_payload = params[:messages].first[message_type.to_sym]
|
||||||
|
@ -89,9 +88,13 @@ class Whatsapp::IncomingMessageService
|
||||||
file_type: file_content_type(message_type),
|
file_type: file_content_type(message_type),
|
||||||
file: {
|
file: {
|
||||||
io: attachment_file,
|
io: attachment_file,
|
||||||
filename: attachment_file,
|
filename: attachment_file.original_filename,
|
||||||
content_type: attachment_file.content_type
|
content_type: attachment_file.content_type
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_type
|
||||||
|
params[:messages].first[:type]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
json.id resource.id
|
json.id resource.id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
json.email resource.email
|
json.email resource.email
|
||||||
|
json.phone_number resource.phone_number
|
||||||
|
|
|
@ -5,6 +5,15 @@ const vue = require('./loaders/vue');
|
||||||
|
|
||||||
environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin());
|
environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin());
|
||||||
environment.loaders.prepend('vue', vue);
|
environment.loaders.prepend('vue', vue);
|
||||||
|
|
||||||
|
environment.loaders.append('opus', {
|
||||||
|
test: /encoderWorker\.min\.js$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
environment.loaders.append('audio', {
|
environment.loaders.append('audio', {
|
||||||
test: /\.(mp3)(\?.*)?$/,
|
test: /\.(mp3)(\?.*)?$/,
|
||||||
loader: 'url-loader',
|
loader: 'url-loader',
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddIndexToConversationAndReportingEvent < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_index :conversations, :last_activity_at
|
||||||
|
add_index :reporting_events, :conversation_id
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_04_24_081117) do
|
ActiveRecord::Schema.define(version: 2022_04_28_101325) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -361,6 +361,7 @@ ActiveRecord::Schema.define(version: 2022_04_24_081117) do
|
||||||
t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id"
|
t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id"
|
||||||
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
||||||
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
||||||
|
t.index ["last_activity_at"], name: "index_conversations_on_last_activity_at"
|
||||||
t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id"
|
t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id"
|
||||||
t.index ["team_id"], name: "index_conversations_on_team_id"
|
t.index ["team_id"], name: "index_conversations_on_team_id"
|
||||||
end
|
end
|
||||||
|
@ -662,6 +663,7 @@ ActiveRecord::Schema.define(version: 2022_04_24_081117) do
|
||||||
t.datetime "event_start_time"
|
t.datetime "event_start_time"
|
||||||
t.datetime "event_end_time"
|
t.datetime "event_end_time"
|
||||||
t.index ["account_id"], name: "index_reporting_events_on_account_id"
|
t.index ["account_id"], name: "index_reporting_events_on_account_id"
|
||||||
|
t.index ["conversation_id"], name: "index_reporting_events_on_conversation_id"
|
||||||
t.index ["created_at"], name: "index_reporting_events_on_created_at"
|
t.index ["created_at"], name: "index_reporting_events_on_created_at"
|
||||||
t.index ["inbox_id"], name: "index_reporting_events_on_inbox_id"
|
t.index ["inbox_id"], name: "index_reporting_events_on_inbox_id"
|
||||||
t.index ["name"], name: "index_reporting_events_on_name"
|
t.index ["name"], name: "index_reporting_events_on_name"
|
||||||
|
|
|
@ -3,43 +3,44 @@
|
||||||
class Integrations::Facebook::MessageParser
|
class Integrations::Facebook::MessageParser
|
||||||
def initialize(response_json)
|
def initialize(response_json)
|
||||||
@response = JSON.parse(response_json)
|
@response = JSON.parse(response_json)
|
||||||
|
@messaging = @response['messaging'] || @response['standby']
|
||||||
end
|
end
|
||||||
|
|
||||||
def sender_id
|
def sender_id
|
||||||
@response.dig 'messaging', 'sender', 'id'
|
@messaging.dig('sender', 'id')
|
||||||
end
|
end
|
||||||
|
|
||||||
def recipient_id
|
def recipient_id
|
||||||
@response.dig 'messaging', 'recipient', 'id'
|
@messaging.dig('recipient', 'id')
|
||||||
end
|
end
|
||||||
|
|
||||||
def time_stamp
|
def time_stamp
|
||||||
@response.dig 'messaging', 'timestamp'
|
@messaging['timestamp']
|
||||||
end
|
end
|
||||||
|
|
||||||
def content
|
def content
|
||||||
@response.dig 'messaging', 'message', 'text'
|
@messaging.dig('message', 'text')
|
||||||
end
|
end
|
||||||
|
|
||||||
def sequence
|
def sequence
|
||||||
@response.dig 'messaging', 'message', 'seq'
|
@messaging.dig('message', 'seq')
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments
|
def attachments
|
||||||
@response.dig 'messaging', 'message', 'attachments'
|
@messaging.dig('message', 'attachments')
|
||||||
end
|
end
|
||||||
|
|
||||||
def identifier
|
def identifier
|
||||||
@response.dig 'messaging', 'message', 'mid'
|
@messaging.dig('message', 'mid')
|
||||||
end
|
end
|
||||||
|
|
||||||
def echo?
|
def echo?
|
||||||
@response.dig 'messaging', 'message', 'is_echo'
|
@messaging.dig('message', 'is_echo')
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO : i don't think the payload contains app_id. if not remove
|
# TODO : i don't think the payload contains app_id. if not remove
|
||||||
def app_id
|
def app_id
|
||||||
@response.dig 'messaging', 'message', 'app_id'
|
@messaging.dig('message', 'app_id')
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO : does this work ?
|
# TODO : does this work ?
|
||||||
|
|
|
@ -34,7 +34,11 @@ class Integrations::Slack::IncomingMessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def supported_message?
|
def supported_message?
|
||||||
SUPPORTED_MESSAGE_TYPES.include?(message[:type]) if message.present?
|
if message.present?
|
||||||
|
SUPPORTED_MESSAGE_TYPES.include?(message[:type])
|
||||||
|
else
|
||||||
|
params[:event][:files].any?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hook_verification?
|
def hook_verification?
|
||||||
|
|
|
@ -46,17 +46,18 @@
|
||||||
"marked": "4.0.10",
|
"marked": "4.0.10",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"ninja-keys": "^1.1.9",
|
"ninja-keys": "^1.1.9",
|
||||||
|
"opus-recorder": "^8.0.5",
|
||||||
"posthog-js": "^1.13.7",
|
"posthog-js": "^1.13.7",
|
||||||
"prosemirror-markdown": "1.5.1",
|
"prosemirror-markdown": "1.5.1",
|
||||||
"prosemirror-state": "1.3.4",
|
"prosemirror-state": "1.3.4",
|
||||||
"prosemirror-view": "1.18.4",
|
"prosemirror-view": "1.18.4",
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
"recordrtc": "^5.6.2",
|
|
||||||
"semver": "7.3.5",
|
"semver": "7.3.5",
|
||||||
"spinkit": "~1.2.5",
|
"spinkit": "~1.2.5",
|
||||||
"tailwindcss": "^1.9.6",
|
"tailwindcss": "^1.9.6",
|
||||||
"url-loader": "^2.0.0",
|
"url-loader": "^2.0.0",
|
||||||
"v-tooltip": "~2.1.3",
|
"v-tooltip": "~2.1.3",
|
||||||
|
"videojs-record": "^4.5.0",
|
||||||
"vue": "2.6.12",
|
"vue": "2.6.12",
|
||||||
"vue-axios": "~1.2.2",
|
"vue-axios": "~1.2.2",
|
||||||
"vue-chartjs": "3.5.1",
|
"vue-chartjs": "3.5.1",
|
||||||
|
@ -75,7 +76,7 @@
|
||||||
"vuelidate": "0.7.6",
|
"vuelidate": "0.7.6",
|
||||||
"vuex": "~2.1.1",
|
"vuex": "~2.1.1",
|
||||||
"vuex-router-sync": "~4.1.2",
|
"vuex-router-sync": "~4.1.2",
|
||||||
"wavesurfer.js": "^5.2.0"
|
"wavesurfer.js": "^6.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.13.16",
|
"@babel/core": "7.13.16",
|
||||||
|
|
|
@ -58,7 +58,11 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
expect(current_day_metric.length).to eq(1)
|
expect(current_day_metric.length).to eq(1)
|
||||||
expect(current_day_metric[0]['value']).to eq(10)
|
expect(current_day_metric[0]['value']).to eq(10)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /api/v2/accounts/:account_id/reports/conversations' do
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
it 'return conversation metrics in account level' do
|
it 'return conversation metrics in account level' do
|
||||||
unassigned_conversation = create(:conversation, account: account, inbox: inbox,
|
unassigned_conversation = create(:conversation, account: account, inbox: inbox,
|
||||||
assignee: nil, created_at: Time.zone.today)
|
assignee: nil, created_at: Time.zone.today)
|
||||||
|
@ -102,25 +106,45 @@ RSpec.describe 'Reports API', type: :request do
|
||||||
expect(user_metrics['metric']['open']).to eq(2)
|
expect(user_metrics['metric']['open']).to eq(2)
|
||||||
expect(user_metrics['metric']['unattended']).to eq(2)
|
expect(user_metrics['metric']['unattended']).to eq(2)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'return conversation metrics for specific user in account level' do
|
context 'when an agent1 associated to conversation having first reply from agent2' do
|
||||||
create_list(:conversation, 2, account: account, inbox: inbox,
|
let(:listener) { ReportingEventListener.instance }
|
||||||
assignee: admin, created_at: Time.zone.today)
|
let(:account) { create(:account) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
|
it 'returns unattended conversation count zero for agent1' do
|
||||||
|
agent1 = create(:user, account: account, role: :agent)
|
||||||
|
agent2 = create(:user, account: account, role: :agent)
|
||||||
|
conversation = create(:conversation, account: account,
|
||||||
|
inbox: inbox, assignee: agent2)
|
||||||
|
|
||||||
|
create(:message, message_type: 'incoming', content: 'Hi',
|
||||||
|
account: account, inbox: inbox,
|
||||||
|
conversation: conversation)
|
||||||
|
first_reply_message = create(:message, message_type: 'outgoing', content: 'Hi',
|
||||||
|
account: account, inbox: inbox, sender: agent2,
|
||||||
|
conversation: conversation)
|
||||||
|
|
||||||
|
event = Events::Base.new('first.reply.created', Time.zone.now, message: first_reply_message)
|
||||||
|
listener.first_reply_created(event)
|
||||||
|
|
||||||
|
conversation.assignee_id = agent1.id
|
||||||
|
conversation.save!
|
||||||
|
|
||||||
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
get "/api/v2/accounts/#{account.id}/reports/conversations",
|
||||||
params: {
|
params: {
|
||||||
type: :agent,
|
type: :agent
|
||||||
user_id: user.id
|
|
||||||
},
|
},
|
||||||
headers: admin.create_new_auth_token,
|
headers: admin.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
|
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response.blank?).to be false
|
user_metrics = json_response.find { |item| item['name'] == agent1[:name] }
|
||||||
expect(json_response[0]['metric']['open']).to eq(10)
|
expect(user_metrics.present?).to be true
|
||||||
expect(json_response[0]['metric']['unattended']).to eq(10)
|
|
||||||
|
expect(user_metrics['metric']['open']).to eq(1)
|
||||||
|
expect(user_metrics['metric']['unattended']).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,8 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||||
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts"
|
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts"
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
data = JSON.parse(response.body)
|
data = response.parsed_body
|
||||||
|
expect(data.keys).to include('email', 'id', 'name', 'phone_number', 'pubsub_token', 'source_id')
|
||||||
expect(data['source_id']).not_to eq nil
|
expect(data['source_id']).not_to eq nil
|
||||||
expect(data['pubsub_token']).not_to eq nil
|
expect(data['pubsub_token']).not_to eq nil
|
||||||
end
|
end
|
||||||
|
@ -21,7 +22,8 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||||
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}"
|
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}"
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
data = JSON.parse(response.body)
|
data = response.parsed_body
|
||||||
|
expect(data.keys).to include('email', 'id', 'name', 'phone_number', 'pubsub_token', 'source_id')
|
||||||
expect(data['source_id']).to eq contact_inbox.source_id
|
expect(data['source_id']).to eq contact_inbox.source_id
|
||||||
expect(data['pubsub_token']).to eq contact_inbox.pubsub_token
|
expect(data['pubsub_token']).to eq contact_inbox.pubsub_token
|
||||||
end
|
end
|
||||||
|
@ -33,7 +35,7 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||||
params: { name: 'John Smith' }
|
params: { name: 'John Smith' }
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
data = JSON.parse(response.body)
|
data = response.parsed_body
|
||||||
expect(data['name']).to eq 'John Smith'
|
expect(data['name']).to eq 'John Smith'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,9 +4,8 @@ describe AutomationRuleListener do
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
let(:inbox) { create(:inbox, account: account) }
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
let(:contact) { create(:contact, account: account, identifier: '123') }
|
let(:contact) { create(:contact, account: account, identifier: '123') }
|
||||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
||||||
let(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: inbox, account: account) }
|
let!(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
|
||||||
let(:automation_rule) { create(:automation_rule, account: account, name: 'Test Automation Rule') }
|
|
||||||
let(:team) { create(:team, account: account) }
|
let(:team) { create(:team, account: account) }
|
||||||
let(:user_1) { create(:user, role: 0) }
|
let(:user_1) { create(:user, role: 0) }
|
||||||
let(:user_2) { create(:user, role: 0) }
|
let(:user_2) { create(:user, role: 0) }
|
||||||
|
@ -214,30 +213,23 @@ describe AutomationRuleListener do
|
||||||
context 'when rule matches' do
|
context 'when rule matches' do
|
||||||
it 'triggers automation rule to assign team' do
|
it 'triggers automation rule to assign team' do
|
||||||
expect(conversation.team_id).not_to eq(team.id)
|
expect(conversation.team_id).not_to eq(team.id)
|
||||||
|
|
||||||
automation_rule
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.team_id).to eq(team.id)
|
expect(conversation.team_id).to eq(team.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule to add label' do
|
it 'triggers automation rule to add label' do
|
||||||
expect(conversation.labels).to eq([])
|
expect(conversation.labels).to eq([])
|
||||||
|
|
||||||
automation_rule
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule to assign best agents' do
|
it 'triggers automation rule to assign best agents' do
|
||||||
expect(conversation.assignee).to be_nil
|
expect(conversation.assignee).to be_nil
|
||||||
|
|
||||||
automation_rule
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.assignee).to eq(user_1)
|
expect(conversation.assignee).to eq(user_1)
|
||||||
|
@ -245,21 +237,14 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
mailer = double
|
mailer = double
|
||||||
|
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
allow(mailer).to receive(:conversation_transcript)
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule send email to the team' do
|
it 'triggers automation rule send email to the team' do
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
@ -267,13 +252,8 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
it 'triggers automation rule send message to the contacts' do
|
it 'triggers automation rule send message to the contacts' do
|
||||||
expect(conversation.messages).to be_empty
|
expect(conversation.messages).to be_empty
|
||||||
|
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.conversation_updated(event)
|
listener.conversation_updated(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.messages.first.content).to eq('Send this message.')
|
expect(conversation.messages.first.content).to eq('Send this message.')
|
||||||
|
@ -299,38 +279,26 @@ describe AutomationRuleListener do
|
||||||
context 'when rule matches' do
|
context 'when rule matches' do
|
||||||
it 'triggers automation rule to assign team' do
|
it 'triggers automation rule to assign team' do
|
||||||
expect(conversation.team_id).not_to eq(team.id)
|
expect(conversation.team_id).not_to eq(team.id)
|
||||||
|
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.team_id).to eq(team.id)
|
expect(conversation.team_id).to eq(team.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule to add label' do
|
it 'triggers automation rule to add label' do
|
||||||
expect(conversation.labels).to eq([])
|
expect(conversation.labels).to eq([])
|
||||||
|
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
expect(conversation.labels.pluck(:name)).to contain_exactly('support', 'priority_customer')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule to assign best agent' do
|
it 'triggers automation rule to assign best agent' do
|
||||||
expect(conversation.assignee).to be_nil
|
expect(conversation.assignee).to be_nil
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.assignee).to eq(user_1)
|
expect(conversation.assignee).to eq(user_1)
|
||||||
|
@ -338,13 +306,8 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
mailer = double
|
mailer = double
|
||||||
|
|
||||||
automation_rule
|
|
||||||
|
|
||||||
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
expect(TeamNotifications::AutomationNotificationMailer).to receive(:conversation_creation)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
allow(mailer).to receive(:conversation_transcript)
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
@ -381,21 +344,18 @@ describe AutomationRuleListener do
|
||||||
context 'when rule matches' do
|
context 'when rule matches' do
|
||||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
mailer = double
|
mailer = double
|
||||||
|
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||||
automation_rule
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
allow(mailer).to receive(:conversation_transcript)
|
expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule send message to the contacts' do
|
it 'triggers automation rule send message to the contacts' do
|
||||||
expect(conversation.messages.count).to eq(1)
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.messages.count).to eq(2)
|
expect(conversation.messages.count).to eq(2)
|
||||||
|
@ -416,9 +376,7 @@ describe AutomationRuleListener do
|
||||||
|
|
||||||
it 'triggers automation rule but wont send message' do
|
it 'triggers automation rule but wont send message' do
|
||||||
expect(conversation.messages.count).to eq(1)
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
|
||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
expect(conversation.messages.count).to eq(1)
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
@ -455,14 +413,14 @@ describe AutomationRuleListener do
|
||||||
context 'when rule matches' do
|
context 'when rule matches' do
|
||||||
it 'triggers automation rule send email transcript to the mentioned email' do
|
it 'triggers automation rule send email transcript to the mentioned email' do
|
||||||
mailer = double
|
mailer = double
|
||||||
|
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||||
automation_rule
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
|
||||||
listener.conversation_created(event)
|
listener.conversation_created(event)
|
||||||
|
|
||||||
conversation.reload
|
conversation.reload
|
||||||
|
|
||||||
allow(mailer).to receive(:conversation_transcript)
|
expect(mailer).to have_received(:conversation_transcript).with(conversation, 'new_agent@example.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'triggers automation rule send message to the contacts' do
|
it 'triggers automation rule send message to the contacts' do
|
||||||
|
@ -501,4 +459,48 @@ describe AutomationRuleListener do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#message_created for tweet events' do
|
||||||
|
before do
|
||||||
|
automation_rule.update!(
|
||||||
|
event_name: 'message_created',
|
||||||
|
name: 'Call actions message created',
|
||||||
|
description: 'Send Message in the conversation',
|
||||||
|
conditions: [
|
||||||
|
{ attribute_key: 'status', filter_operator: 'equal_to', values: ['open'], query_operator: nil }.with_indifferent_access
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{ 'action_name' => 'send_message', 'action_params' => ['Send this message.'] },
|
||||||
|
{ 'action_name' => 'send_attachment', 'action_params' => [123] },
|
||||||
|
{ 'action_name' => 'send_email_transcript', 'action_params' => ['new_agent@example.com'] }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rule matches' do
|
||||||
|
let(:tweet) { create(:conversation, additional_attributes: { type: 'tweet' }, inbox: inbox, account: account) }
|
||||||
|
let(:event) { Events::Base.new('message_created', Time.zone.now, { conversation: tweet, message: message }) }
|
||||||
|
let!(:message) { create(:message, account: account, conversation: tweet, message_type: 'incoming') }
|
||||||
|
|
||||||
|
it 'triggers automation rule except send_message and send_attachment' do
|
||||||
|
mailer = double
|
||||||
|
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
|
||||||
|
allow(mailer).to receive(:conversation_transcript)
|
||||||
|
|
||||||
|
listener.message_created(event)
|
||||||
|
expect(mailer).to have_received(:conversation_transcript).with(tweet, 'new_agent@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not triggers automation rule send message or send attachment' do
|
||||||
|
expect(tweet.messages.count).to eq(1)
|
||||||
|
|
||||||
|
listener.message_created(event)
|
||||||
|
|
||||||
|
tweet.reload
|
||||||
|
|
||||||
|
expect(tweet.messages.count).to eq(1)
|
||||||
|
expect(tweet.messages.last.content).to eq(message.content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,17 +22,7 @@ module SlackStubs
|
||||||
end
|
end
|
||||||
|
|
||||||
def slack_attachment_stub
|
def slack_attachment_stub
|
||||||
{
|
slack_message_stub.merge({ event: message_event_without_blocks })
|
||||||
token: '[FILTERED]',
|
|
||||||
team_id: 'TLST3048H',
|
|
||||||
api_app_id: 'A012S5UETV4',
|
|
||||||
event: message_event,
|
|
||||||
type: 'event_callback',
|
|
||||||
event_id: 'Ev013QUX3WV6',
|
|
||||||
event_time: 1_588_623_033,
|
|
||||||
authed_users: '[FILTERED]',
|
|
||||||
webhook: {}
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def slack_message_stub_without_thread_ts
|
def slack_message_stub_without_thread_ts
|
||||||
|
@ -95,15 +85,26 @@ module SlackStubs
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
type: 'rich_text_section',
|
type: 'rich_text_section',
|
||||||
elements: [
|
elements: [{
|
||||||
{
|
type: 'text',
|
||||||
type: 'text',
|
text: 'this is test'
|
||||||
text: 'this is test'
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_event_without_blocks
|
||||||
|
{
|
||||||
|
client_msg_id: 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
|
||||||
|
type: 'message',
|
||||||
|
text: 'this is test <https://chatwoot.com> Hey <@U019KT237LP|Sojan> Test again',
|
||||||
|
user: 'ULYPAKE5S',
|
||||||
|
ts: '1588623033.006000',
|
||||||
|
files: file_stub,
|
||||||
|
thread_ts: '1588623023.005900',
|
||||||
|
channel: 'G01354F6A6Q'
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,9 +10,12 @@ properties:
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
description: Email of the contact
|
description: Email of the contact
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: Name of the contact
|
description: Name of the contact
|
||||||
|
phone_number:
|
||||||
|
type: string
|
||||||
|
description: Phone number of the contact
|
||||||
avatar_url:
|
avatar_url:
|
||||||
type: string
|
type: string
|
||||||
description: The url to a jpeg, png file for the user avatar
|
description: The url to a jpeg, png file for the user avatar
|
||||||
|
|
|
@ -5268,6 +5268,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name of the contact"
|
"description": "Name of the contact"
|
||||||
},
|
},
|
||||||
|
"phone_number": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Phone number of the contact"
|
||||||
|
},
|
||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The url to a jpeg, png file for the user avatar"
|
"description": "The url to a jpeg, png file for the user avatar"
|
||||||
|
|
Loading…
Reference in a new issue