Merge branch 'develop' into heatmap-last-30-days
This commit is contained in:
+3
-3
@@ -186,7 +186,7 @@ GEM
|
||||
database_cleaner-core (2.0.1)
|
||||
datadog-ci (0.8.3)
|
||||
msgpack
|
||||
date (3.3.4)
|
||||
date (3.4.1)
|
||||
ddtrace (1.23.2)
|
||||
datadog-ci (~> 0.8.1)
|
||||
debase-ruby_core_source (= 3.3.1)
|
||||
@@ -487,7 +487,7 @@ GEM
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.17)
|
||||
net-imap (0.4.19)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -782,7 +782,7 @@ GEM
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
timeout (0.4.1)
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
twilio-ruby (5.77.0)
|
||||
faraday (>= 0.9, < 3.0)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.1.0
|
||||
3.2.0
|
||||
|
||||
+3
-2
@@ -77,7 +77,7 @@ const toggleMessageSignature = () => {
|
||||
setSignature();
|
||||
};
|
||||
|
||||
// Added this watch to dynamically set signature.
|
||||
// Added this watch to dynamically set signature on target inbox change.
|
||||
// Only targetInbox has value and is Advance Editor(used by isEmailOrWebWidgetInbox)
|
||||
// Set the signature only if the inbox based flag is true
|
||||
watch(
|
||||
@@ -86,7 +86,8 @@ watch(
|
||||
nextTick(() => {
|
||||
if (newValue && props.isEmailOrWebWidgetInbox) setSignature();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const onClickInsertEmoji = emoji => {
|
||||
|
||||
@@ -15,18 +15,14 @@ import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
||||
* @property {Array} messages - Array of all messages [These are not in camelcase]
|
||||
*/
|
||||
const props = defineProps({
|
||||
readMessages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
unReadMessages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
currentUserId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
firstUnreadId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
isAnEmailChannel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -41,12 +37,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const unread = computed(() => {
|
||||
return useCamelCase(props.unReadMessages, { deep: true });
|
||||
});
|
||||
|
||||
const read = computed(() => {
|
||||
return useCamelCase(props.readMessages, { deep: true });
|
||||
const allMessages = computed(() => {
|
||||
return useCamelCase(props.messages, { deep: true });
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -108,26 +100,18 @@ const getInReplyToMessage = parentMessage => {
|
||||
<template>
|
||||
<ul class="px-4 bg-n-background">
|
||||
<slot name="beforeAll" />
|
||||
<template v-for="(message, index) in read" :key="message.id">
|
||||
<Message
|
||||
v-bind="message"
|
||||
:is-email-inbox="isAnEmailChannel"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
:group-with-next="shouldGroupWithNext(index, read)"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:current-user-id="currentUserId"
|
||||
data-clarity-mask="True"
|
||||
<template v-for="(message, index) in allMessages" :key="message.id">
|
||||
<slot
|
||||
v-if="firstUnreadId && message.id === firstUnreadId"
|
||||
name="unreadBadge"
|
||||
/>
|
||||
</template>
|
||||
<slot name="beforeUnread" />
|
||||
<template v-for="(message, index) in unread" :key="message.id">
|
||||
<Message
|
||||
v-bind="message"
|
||||
:is-email-inbox="isAnEmailChannel"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
:group-with-next="shouldGroupWithNext(index, unread)"
|
||||
:group-with-next="shouldGroupWithNext(index, allMessages)"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:current-user-id="currentUserId"
|
||||
:is-email-inbox="isAnEmailChannel"
|
||||
data-clarity-mask="True"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -38,9 +38,10 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const formatTime = time => {
|
||||
if (!time || Number.isNaN(time)) return '00:00';
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
@@ -49,7 +50,7 @@ const toggleMute = () => {
|
||||
};
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
currentTime.value = audioPlayer.value.currentTime;
|
||||
currentTime.value = audioPlayer.value?.currentTime;
|
||||
};
|
||||
|
||||
const seek = event => {
|
||||
|
||||
@@ -243,6 +243,15 @@ export default {
|
||||
unreadMessageCount() {
|
||||
return this.currentChat.unread_count || 0;
|
||||
},
|
||||
unreadMessageLabel() {
|
||||
const count =
|
||||
this.unreadMessageCount > 9 ? '9+' : this.unreadMessageCount;
|
||||
const label =
|
||||
this.unreadMessageCount > 1
|
||||
? 'CONVERSATION.UNREAD_MESSAGES'
|
||||
: 'CONVERSATION.UNREAD_MESSAGE';
|
||||
return `${count} ${this.$t(label)}`;
|
||||
},
|
||||
isInstagramDM() {
|
||||
return this.conversationType === 'instagram_direct_message';
|
||||
},
|
||||
@@ -492,12 +501,11 @@ export default {
|
||||
<NextMessageList
|
||||
v-if="showNextBubbles"
|
||||
class="conversation-panel"
|
||||
:read-messages="readMessages"
|
||||
:un-read-messages="unReadMessages"
|
||||
:current-user-id="currentUserId"
|
||||
:first-unread-id="unReadMessages[0]?.id"
|
||||
:is-an-email-channel="isAnEmailChannel"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:messages="currentChat ? currentChat.messages : []"
|
||||
:messages="getMessages"
|
||||
>
|
||||
<template #beforeAll>
|
||||
<transition name="slide-up">
|
||||
@@ -507,15 +515,10 @@ export default {
|
||||
</li>
|
||||
</transition>
|
||||
</template>
|
||||
<template #beforeUnread>
|
||||
<template #unreadBadge>
|
||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||
<span>
|
||||
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
|
||||
{{
|
||||
unreadMessageCount > 1
|
||||
? $t('CONVERSATION.UNREAD_MESSAGES')
|
||||
: $t('CONVERSATION.UNREAD_MESSAGE')
|
||||
}}
|
||||
{{ unreadMessageLabel }}
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
},
|
||||
"fallback": {
|
||||
"CONTENT": "has shared a url"
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"READ_MORE": "Read more",
|
||||
"WROTE": "wrote:",
|
||||
"FROM": "from",
|
||||
"EMAIL": "email"
|
||||
"EMAIL": "email",
|
||||
"EMAIL_SUBJECT": "subject"
|
||||
}
|
||||
}
|
||||
|
||||
+32
-14
@@ -35,6 +35,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
emailSubject: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const navigateTo = computed(() => {
|
||||
@@ -49,6 +53,28 @@ const navigateTo = computed(() => {
|
||||
});
|
||||
|
||||
const createdAtTime = dynamicTime(props.createdAt);
|
||||
|
||||
const infoItems = computed(() => [
|
||||
{
|
||||
label: 'SEARCH.FROM',
|
||||
value: props.name,
|
||||
show: !!props.name,
|
||||
},
|
||||
{
|
||||
label: 'SEARCH.EMAIL',
|
||||
value: props.email,
|
||||
show: !!props.email,
|
||||
},
|
||||
{
|
||||
label: 'SEARCH.EMAIL_SUBJECT',
|
||||
value: props.emailSubject,
|
||||
show: !!props.emailSubject,
|
||||
},
|
||||
]);
|
||||
|
||||
const visibleInfoItems = computed(() =>
|
||||
infoItems.value.filter(item => item.show)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,26 +112,18 @@ const createdAtTime = dynamicTime(props.createdAt);
|
||||
{{ createdAtTime }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-x-2 gap-y-1.5">
|
||||
<h5
|
||||
v-if="name"
|
||||
class="m-0 text-sm min-w-0 truncate text-n-slate-12 dark:text-n-slate-12"
|
||||
>
|
||||
<span class="text-xs font-norma text-n-slate-11 dark:text-n-slate-11">
|
||||
{{ $t('SEARCH.FROM') }}:
|
||||
</span>
|
||||
{{ name }}
|
||||
</h5>
|
||||
<h5
|
||||
v-if="email"
|
||||
class="m-0 overflow-hidden text-sm text-n-slate-12 dark:text-n-slate-12 truncate"
|
||||
v-for="item in visibleInfoItems"
|
||||
:key="item.label"
|
||||
class="m-0 text-sm min-w-0 text-n-slate-12 dark:text-n-slate-12 truncate"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
|
||||
>
|
||||
{{ $t('SEARCH.EMAIL') }}:
|
||||
{{ $t(item.label) }}:
|
||||
</span>
|
||||
{{ email }}
|
||||
{{ item.value }}
|
||||
</h5>
|
||||
</div>
|
||||
<slot />
|
||||
|
||||
+14
-2
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { defineProps, computed } from 'vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import SearchResultSection from './SearchResultSection.vue';
|
||||
import SearchResultConversationItem from './SearchResultConversationItem.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
conversations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -23,6 +24,13 @@ defineProps({
|
||||
});
|
||||
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
const conversationsWithSubject = computed(() => {
|
||||
return props.conversations.map(conversation => ({
|
||||
...conversation,
|
||||
mail_subject: conversation.additional_attributes?.mail_subject || '',
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,7 +42,10 @@ const accountId = useMapGetter('getCurrentAccountId');
|
||||
:is-fetching="isFetching"
|
||||
>
|
||||
<ul v-if="conversations.length" class="space-y-1.5 list-none">
|
||||
<li v-for="conversation in conversations" :key="conversation.id">
|
||||
<li
|
||||
v-for="conversation in conversationsWithSubject"
|
||||
:key="conversation.id"
|
||||
>
|
||||
<SearchResultConversationItem
|
||||
:id="conversation.id"
|
||||
:name="conversation.contact.name"
|
||||
@@ -42,6 +53,7 @@ const accountId = useMapGetter('getCurrentAccountId');
|
||||
:account-id="accountId"
|
||||
:inbox="conversation.inbox"
|
||||
:created-at="conversation.created_at"
|
||||
:email-subject="conversation.mail_subject"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -42,7 +42,8 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
||||
contact_last_seen_at: contact_last_seen_at.to_i,
|
||||
last_activity_at: last_activity_at.to_i,
|
||||
timestamp: last_activity_at.to_i,
|
||||
created_at: created_at.to_i
|
||||
created_at: created_at.to_i,
|
||||
updated_at: updated_at.to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,6 +43,7 @@ class Telegram::IncomingMessageService
|
||||
def process_message_attachments
|
||||
attach_location
|
||||
attach_files
|
||||
attach_contact
|
||||
end
|
||||
|
||||
def update_contact_avatar
|
||||
@@ -136,6 +137,16 @@ class Telegram::IncomingMessageService
|
||||
)
|
||||
end
|
||||
|
||||
def attach_contact
|
||||
return unless contact_card
|
||||
|
||||
@message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: :contact,
|
||||
fallback_title: contact_card['phone_number'].to_s
|
||||
)
|
||||
end
|
||||
|
||||
def file
|
||||
@file ||= visual_media_params || params[:message][:voice].presence || params[:message][:audio].presence || params[:message][:document].presence
|
||||
end
|
||||
@@ -154,6 +165,10 @@ class Telegram::IncomingMessageService
|
||||
@location ||= params.dig(:message, :location).presence
|
||||
end
|
||||
|
||||
def contact_card
|
||||
@contact_card ||= params.dig(:message, :contact).presence
|
||||
end
|
||||
|
||||
def visual_media_params
|
||||
params[:message][:photo].presence&.last || params.dig(:message, :sticker, :thumb).presence || params[:message][:video].presence
|
||||
end
|
||||
|
||||
@@ -16,6 +16,8 @@ json.payload do
|
||||
json.agent do
|
||||
json.partial! 'agent', formats: [:json], agent: conversation.assignee if conversation.try(:assignee).present?
|
||||
end
|
||||
|
||||
json.additional_attributes conversation.additional_attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,6 +44,7 @@ json.muted conversation.muted?
|
||||
json.snoozed_until conversation.snoozed_until
|
||||
json.status conversation.status
|
||||
json.created_at conversation.created_at.to_i
|
||||
json.updated_at conversation.updated_at.to_i
|
||||
json.timestamp conversation.last_activity_at.to_i
|
||||
json.first_reply_created_at conversation.first_reply_created_at.to_i
|
||||
json.unread_count conversation.unread_incoming_messages.count
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
Finish Setup
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Description: Install and manage a Chatwoot installation.
|
||||
# OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
|
||||
# Script Version: 3.1.0
|
||||
# Script Version: 3.2.0
|
||||
# Run this script as root
|
||||
|
||||
set -eu -o errexit -o pipefail -o noclobber -o nounset
|
||||
@@ -19,7 +19,7 @@ fi
|
||||
# option --output/-o requires 1 argument
|
||||
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
|
||||
OPTIONS=cdhiI:l:rsuwv
|
||||
CWCTL_VERSION="3.1.0"
|
||||
CWCTL_VERSION="3.2.0"
|
||||
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||
CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events"
|
||||
|
||||
|
||||
@@ -538,6 +538,7 @@ RSpec.describe Conversation do
|
||||
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
||||
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
||||
created_at: conversation.created_at.to_i,
|
||||
updated_at: conversation.updated_at.to_i,
|
||||
waiting_since: conversation.waiting_since.to_i,
|
||||
priority: nil,
|
||||
unread_count: 0
|
||||
|
||||
@@ -31,6 +31,7 @@ RSpec.describe Conversations::EventDataPresenter do
|
||||
contact_last_seen_at: conversation.contact_last_seen_at.to_i,
|
||||
agent_last_seen_at: conversation.agent_last_seen_at.to_i,
|
||||
created_at: conversation.created_at.to_i,
|
||||
updated_at: conversation.updated_at.to_i,
|
||||
waiting_since: conversation.waiting_since.to_i,
|
||||
priority: nil,
|
||||
unread_count: 0
|
||||
|
||||
@@ -309,5 +309,22 @@ describe Telegram::IncomingMessageService do
|
||||
expect(telegram_channel.inbox.messages.first.content).to eq('Option 1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid contact message params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
params = {
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'message' => {
|
||||
'contact': {
|
||||
'phone_number': '+918660944581'
|
||||
}
|
||||
}.merge(message_params)
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('contact')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user