+
-
- {{ identifier }}
-
-
+
+
+
+ {{ identifier }}
+
+
+
+
-
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue b/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue
new file mode 100644
index 000000000..160394142
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/linear/IssuesList.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('INTEGRATION_SETTINGS.LINEAR.NO_LINKED_ISSUES') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue b/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue
new file mode 100644
index 000000000..e9d1ca500
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/linear/LinearIssueItem.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+ {{ issue.title }}
+
+
+
+ {{ issue.description }}
+
+
+
+
+
+
+
+
+ {{ assignee.name }}
+
+
+
+
+
+
+
+
+ {{ issue.state?.name }}
+
+
+
+
+
+
+
+
+ {{ priorityLabel }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/LinearSetupCTA.vue b/app/javascript/dashboard/components/widgets/conversation/linear/LinearSetupCTA.vue
new file mode 100644
index 000000000..f7394a092
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/linear/LinearSetupCTA.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
![]()
+
![]()
+
+
+
+
+ {{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.TITLE') }}
+
+
+ {{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.DESCRIPTION') }}
+
+
+ {{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.AGENT_DESCRIPTION') }}
+
+
+
+
+ {{ $t('INTEGRATION_SETTINGS.LINEAR.CTA.BUTTON_TEXT') }}
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/linear/index.vue b/app/javascript/dashboard/components/widgets/conversation/linear/index.vue
deleted file mode 100644
index f7c12fcdf..000000000
--- a/app/javascript/dashboard/components/widgets/conversation/linear/index.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/composables/useUISettings.js b/app/javascript/dashboard/composables/useUISettings.js
index 29fe878c7..964c615ad 100644
--- a/app/javascript/dashboard/composables/useUISettings.js
+++ b/app/javascript/dashboard/composables/useUISettings.js
@@ -9,6 +9,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'contact_notes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
+ { name: 'linear_issues' },
{ name: 'shopify_orders' },
]);
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index fdb7fc07a..c047f17ad 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -308,6 +308,7 @@
"CONTACT_ATTRIBUTES": "Contact Attributes",
"PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros",
+ "LINEAR_ISSUES": "Linked Linear Issues",
"SHOPIFY_ORDERS": "Shopify Orders"
},
"SHOPIFY": {
diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json
index cfa46867c..071c95604 100644
--- a/app/javascript/dashboard/i18n/locale/en/integrations.json
+++ b/app/javascript/dashboard/i18n/locale/en/integrations.json
@@ -315,11 +315,18 @@
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
},
+ "NO_LINKED_ISSUES": "No linked issues found",
"DELETE": {
"TITLE": "Are you sure you want to delete the integration?",
"MESSAGE": "Are you sure you want to delete the integration?",
"CONFIRM": "Yes, delete",
"CANCEL": "Cancel"
+ },
+ "CTA": {
+ "TITLE": "Connect to Linear",
+ "AGENT_DESCRIPTION": "Linear workspace is not connected. Request your administrator to connect a workspace to use this integration.",
+ "DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.",
+ "BUTTON_TEXT": "Connect Linear workspace"
}
}
},
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
index 212d9219b..d7f99802c 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
@@ -19,6 +19,9 @@ import Draggable from 'vuedraggable';
import MacrosList from './Macros/List.vue';
import ShopifyOrdersList from 'dashboard/components/widgets/conversation/ShopifyOrdersList.vue';
import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue';
+import LinearIssuesList from 'dashboard/components/widgets/conversation/linear/IssuesList.vue';
+import LinearSetupCTA from 'dashboard/components/widgets/conversation/linear/LinearSetupCTA.vue';
+import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const props = defineProps({
conversationId: {
@@ -40,6 +43,13 @@ const {
const dragging = ref(false);
const conversationSidebarItems = ref([]);
+
+const currentAccountId = useMapGetter('getCurrentAccountId');
+
+const isFeatureEnabledonAccount = useMapGetter(
+ 'accounts/isFeatureEnabledonAccount'
+);
+
const shopifyIntegration = useFunctionGetter(
'integrations/getIntegration',
'shopify'
@@ -49,6 +59,20 @@ const isShopifyFeatureEnabled = computed(
() => shopifyIntegration.value.enabled
);
+const linearIntegration = useFunctionGetter(
+ 'integrations/getIntegration',
+ 'linear'
+);
+
+const isLinearIntegrationEnabled = computed(
+ () => linearIntegration.value?.enabled || false
+);
+
+const isLinearFeatureEnabled = isFeatureEnabledonAccount.value(
+ currentAccountId.value,
+ FEATURE_FLAGS.LINEAR
+);
+
const store = useStore();
const currentChat = useMapGetter('getSelectedChat');
const conversationId = computed(() => props.conversationId);
@@ -103,6 +127,8 @@ onMounted(() => {
conversationSidebarItems.value = conversationSidebarItemsOrder.value;
getContactDetails();
store.dispatch('attributes/get', 0);
+ // Load integrations to ensure linear integration state is available
+ store.dispatch('integrations/get', 'linear');
});
@@ -113,7 +139,7 @@ onMounted(() => {
@close="closeContactPanel"
/>
-
+
{
@end="onDragEnd"
>
-
-
-
toggleSidebarUIState('is_conv_actions_open', value)
- "
- >
-
-
-
-
-
- toggleSidebarUIState('is_conv_participants_open', value)
- "
- >
-
-
-
-
-
toggleSidebarUIState('is_conv_details_open', value)
- "
- >
-
-
-
-
-
- toggleSidebarUIState('is_contact_attributes_open', value)
- "
- >
-
-
-
-
-
toggleSidebarUIState('is_previous_conv_open', value)
- "
- >
-
-
-
-
- toggleSidebarUIState('is_macro_open', value)"
- >
-
-
-
-
+
toggleSidebarUIState('is_conv_actions_open', value)
"
>
- toggleSidebarUIState('is_shopify_orders_open', value)
+
+
+
+
+
+ toggleSidebarUIState('is_conv_participants_open', value)
+ "
+ >
+
+
+
+
+
toggleSidebarUIState('is_conv_details_open', value)
+ "
+ >
+
+
+
+
+
+ toggleSidebarUIState('is_contact_attributes_open', value)
+ "
+ >
+
-
-
-
-
-
toggleSidebarUIState('is_contact_notes_open', value)
- "
- >
-
-
-
+ />
+
+
+
+
toggleSidebarUIState('is_previous_conv_open', value)
+ "
+ >
+
+
+
+
+ toggleSidebarUIState('is_macro_open', value)"
+ >
+
+
+
+
+
toggleSidebarUIState('is_linear_issues_open', value)
+ "
+ >
+
+
+
+
+
+
toggleSidebarUIState('is_shopify_orders_open', value)
+ "
+ >
+
+
+
+
+
toggleSidebarUIState('is_contact_notes_open', value)
+ "
+ >
+
+
From c0d9533b3a3a5bfb07584ff23cc68290ff939021 Mon Sep 17 00:00:00 2001
From: Uchkun Rakhimov
Date: Wed, 11 Jun 2025 02:31:28 +0500
Subject: [PATCH 4/4] feat: add support for telegram circular video messages
(#11504)
This PR adds support for handling Telegram's video_note messages (circular video messages), which are commonly used for short voice-style video replies in messaging.
Fixes: #11503
Co-authored-by: Sojan Jose
---
.../telegram/incoming_message_service.rb | 23 ++++++++++++---
.../telegram/incoming_message_service_spec.rb | 29 +++++++++++++++++++
2 files changed, 48 insertions(+), 4 deletions(-)
diff --git a/app/services/telegram/incoming_message_service.rb b/app/services/telegram/incoming_message_service.rb
index e337a03f6..3a186676e 100644
--- a/app/services/telegram/incoming_message_service.rb
+++ b/app/services/telegram/incoming_message_service.rb
@@ -94,13 +94,25 @@ class Telegram::IncomingMessageService
end
def file_content_type
- return :image if params[:message][:photo].present? || params.dig(:message, :sticker, :thumb).present?
- return :audio if params[:message][:voice].present? || params[:message][:audio].present?
- return :video if params[:message][:video].present?
+ return :image if image_message?
+ return :audio if audio_message?
+ return :video if video_message?
file_type(params[:message][:document][:mime_type])
end
+ def image_message?
+ params[:message][:photo].present? || params.dig(:message, :sticker, :thumb).present?
+ end
+
+ def audio_message?
+ params[:message][:voice].present? || params[:message][:audio].present?
+ end
+
+ def video_message?
+ params[:message][:video].present? || params[:message][:video_note].present?
+ end
+
def attach_files
return unless file
@@ -174,6 +186,9 @@ class Telegram::IncomingMessageService
end
def visual_media_params
- params[:message][:photo].presence&.last || params.dig(:message, :sticker, :thumb).presence || params[:message][:video].presence
+ params[:message][:photo].presence&.last ||
+ params.dig(:message, :sticker, :thumb).presence ||
+ params[:message][:video].presence ||
+ params[:message][:video_note].presence
end
end
diff --git a/spec/services/telegram/incoming_message_service_spec.rb b/spec/services/telegram/incoming_message_service_spec.rb
index 0702baa32..78a8e9d7c 100644
--- a/spec/services/telegram/incoming_message_service_spec.rb
+++ b/spec/services/telegram/incoming_message_service_spec.rb
@@ -179,6 +179,35 @@ describe Telegram::IncomingMessageService do
end
end
+ context 'when valid video_note messages params' do
+ it 'creates appropriate conversations, message and contacts' do
+ allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mov')
+ params = {
+ 'update_id' => 2_342_342_343_242,
+ 'message' => {
+ 'video_note' => {
+ 'duration' => 3,
+ 'length' => 240,
+ 'thumb' => {
+ 'file_id' => 'AAMCBQADGQEAA4ZhXd78Xz6_c6gCzbdIkgGiXJcwwwACqwMAAp3x8Fbhf3EWamgCWAEAB20AAyEE',
+ 'file_unique_id' => 'AQADqwMAAp3x8FZy',
+ 'file_size' => 11_462,
+ 'width' => 240,
+ 'height' => 240
+ },
+ 'file_id' => 'DQACAgUAAxkBAAIBY2FdJlhf8PC2E3IalXSvXWO5m8GBAALJAwACwqHgVhb0truM0uhwIQQ',
+ 'file_unique_id' => 'AgADyQMAAsKh4FY',
+ 'file_size' => 132_446
+ }
+ }.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('video')
+ end
+ end
+
context 'when valid voice attachment params' do
it 'creates appropriate conversations, message and contacts' do
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.ogg')