Merge branch 'develop' into feat/new-auth-screens

This commit is contained in:
Sivin Varghese 2022-05-03 21:30:11 +05:30 committed by GitHub
commit 1eccf79cf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 424 additions and 349 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,17 +31,30 @@ 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,
controls: true,
bigPlayButton: false,
fluid: false,
controlBar: {
deviceButton: false,
fullscreenToggle: false,
cameraButton: false,
volumePanel: false,
},
plugins: {
wavesurfer: {
backend: 'WebAudio', backend: 'WebAudio',
interact: true, waveColor: '#1f93ff',
progressColor: 'rgb(25, 118, 204)',
cursorColor: 'rgba(43, 51, 63, 0.7)',
backgroundColor: 'none',
barWidth: 1,
cursorWidth: 1, cursorWidth: 1,
hideScrollbar: true,
plugins: [ plugins: [
WaveSurfer.microphone.create({ WaveSurfer.microphone.create({
bufferSize: 4096, bufferSize: 4096,
@ -41,81 +67,80 @@ export default {
}), }),
], ],
}, },
optionsRecorder: { record: {
type: 'audio', audio: true,
mimeType: 'audio/wav', video: false,
disableLogs: true, displayMilliseconds: false,
recorderType: RecordRTC.StereoAudioRecorder, maxLength: 300,
sampleRate: 44100, audioEngine: 'opus-recorder',
numberOfAudioChannels: 2, audioWorkerURL: encoderWorker,
checkForInactiveTracks: true, audioChannels: 1,
bufferSize: 4096, 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>

View file

@ -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,12 +585,14 @@ export default {
} }
}, },
toggleAudioRecorderPlayPause() { toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio && !this.isRecorderAudioStopped) { if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true; this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording(); this.$refs.audioRecorderInput.stopAudioRecording();
} else if (this.isRecordingAudio && this.isRecorderAudioStopped) { } else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause(); this.$refs.audioRecorderInput.playPause();
} }
}
}, },
hideEmojiPicker() { hideEmojiPicker() {
if (this.showEmojiPicker) { if (this.showEmojiPicker) {
@ -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;

View file

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

View file

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

View file

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

View 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)
# #

View file

@ -18,6 +18,7 @@
# Indexes # Indexes
# #
# index_reporting_events_on_account_id (account_id) # index_reporting_events_on_account_id (account_id)
# index_reporting_events_on_conversation_id (conversation_id)
# index_reporting_events_on_created_at (created_at) # index_reporting_events_on_created_at (created_at)
# index_reporting_events_on_inbox_id (inbox_id) # index_reporting_events_on_inbox_id (inbox_id)
# index_reporting_events_on_name (name) # index_reporting_events_on_name (name)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,9 @@ properties:
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

View file

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