diff --git a/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue
index aa17dabf9..6b356f4d3 100644
--- a/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/components/GalleryView.vue
@@ -22,6 +22,10 @@ const props = defineProps({
type: Array,
required: true,
},
+ autoPlay: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['close']);
@@ -309,6 +313,7 @@ onMounted(() => {
:src="activeAttachment.data_url"
controls
playsInline
+ :autoplay="autoPlay"
class="max-h-full max-w-full object-contain"
@click.stop
/>
@@ -317,6 +322,7 @@ onMounted(() => {
v-if="isAudio"
:key="activeAttachment.message_id"
controls
+ :autoplay="autoPlay"
class="w-full max-w-md"
@click.stop
>
diff --git a/app/javascript/dashboard/composables/useUISettings.js b/app/javascript/dashboard/composables/useUISettings.js
index 9f1943414..4f015e1d8 100644
--- a/app/javascript/dashboard/composables/useUISettings.js
+++ b/app/javascript/dashboard/composables/useUISettings.js
@@ -7,6 +7,7 @@ export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'contact_notes' },
+ { name: 'shared_files' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
{ name: 'linear_issues' },
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index 88e66ebe3..2dc3ed70e 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -365,7 +365,19 @@
"PREVIOUS_CONVERSATION": "Previous Conversations",
"MACROS": "Macros",
"LINEAR_ISSUES": "Linked Linear Issues",
- "SHOPIFY_ORDERS": "Shopify Orders"
+ "SHOPIFY_ORDERS": "Shopify Orders",
+ "SHARED_FILES": "Attachments"
+ },
+ "SHARED_FILES": {
+ "EMPTY": "No attachments yet",
+ "DOWNLOAD": "Download file",
+ "DOWNLOAD_ERROR": "Could not download the file. Please try again.",
+ "MEDIA_HEADING": "Media",
+ "FILES_HEADING": "Files",
+ "VIEW_ALL": "View all",
+ "SHOW_LESS": "Show less",
+ "MORE_COUNT": "+{count}",
+ "UNTITLED_FILE": "Untitled file"
},
"SHOPIFY": {
"ORDER_ID": "Order #{id}",
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
index 653b43840..fe95f9e99 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue
@@ -17,6 +17,7 @@ import ContactInfo from './contact/ContactInfo.vue';
import ContactNotes from './contact/ContactNotes.vue';
import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue';
+import SharedFiles from './SharedFiles.vue';
import Draggable from 'vuedraggable';
import MacrosList from './Macros/List.vue';
import ShopifyOrdersList from 'dashboard/components/widgets/conversation/ShopifyOrdersList.vue';
@@ -297,6 +298,18 @@ onMounted(() => {
+
+
toggleSidebarUIState('is_shared_files_open', value)
+ "
+ >
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/SharedFiles.vue b/app/javascript/dashboard/routes/dashboard/conversation/SharedFiles.vue
new file mode 100644
index 000000000..272407a53
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/conversation/SharedFiles.vue
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+
+
+ {{ t('CONVERSATION_SIDEBAR.SHARED_FILES.EMPTY') }}
+
+
+
+
+
+ {{ t('CONVERSATION_SIDEBAR.SHARED_FILES.MEDIA_HEADING') }}
+
+ {{ mediaAttachments.length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ displayDuration(attachment) }}
+
+
+
+ {{ displayTime(attachment) }}
+
+
+
+
+
+
+
+ {{
+ t('CONVERSATION_SIDEBAR.SHARED_FILES.MORE_COUNT', {
+ count: mediaOverflow,
+ })
+ }}
+
+
+
+
+
+
+
+
+
+ {{ t('CONVERSATION_SIDEBAR.SHARED_FILES.FILES_HEADING') }}
+
+ {{ fileAttachments.length }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js
index 333009707..5e85c423c 100644
--- a/app/javascript/dashboard/store/modules/conversations/getters.js
+++ b/app/javascript/dashboard/store/modules/conversations/getters.js
@@ -57,6 +57,8 @@ const getters = {
getSelectedChatAttachments: ({ selectedChatId, attachments }) => {
return attachments[selectedChatId] || [];
},
+ getSelectedChatAttachmentsLoaded: ({ selectedChatId, attachments }) =>
+ selectedChatId !== null && attachments[selectedChatId] !== undefined,
getChatListFilters: ({ conversationFilters }) => conversationFilters,
getLastEmailInSelectedChat: (stage, _getters) => {
const selectedChat = _getters.getSelectedChat;
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
index 69bf9c2ac..a7ee3833e 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
@@ -328,6 +328,31 @@ describe('#getters', () => {
});
});
+ describe('#getSelectedChatAttachmentsLoaded', () => {
+ it('returns true when attachments have been fetched for the selected chat', () => {
+ const state = { selectedChatId: 1, attachments: { 1: [] } };
+ expect(getters.getSelectedChatAttachmentsLoaded(state)).toBe(true);
+ });
+
+ it('returns true when the fetched attachment list is non-empty', () => {
+ const state = {
+ selectedChatId: 1,
+ attachments: { 1: [{ id: 1, file_name: 'test' }] },
+ };
+ expect(getters.getSelectedChatAttachmentsLoaded(state)).toBe(true);
+ });
+
+ it('returns false when attachments have not been fetched yet', () => {
+ const state = { selectedChatId: 1, attachments: {} };
+ expect(getters.getSelectedChatAttachmentsLoaded(state)).toBe(false);
+ });
+
+ it('returns false when no chat is selected', () => {
+ const state = { selectedChatId: null, attachments: {} };
+ expect(getters.getSelectedChatAttachmentsLoaded(state)).toBe(false);
+ });
+ });
+
describe('#getContextMenuChatId', () => {
it('returns the context menu chat id', () => {
const state = { contextMenuChatId: 1 };
diff --git a/app/views/api/v1/accounts/conversations/attachments.json.jbuilder b/app/views/api/v1/accounts/conversations/attachments.json.jbuilder
index 167b18390..8bd647f27 100644
--- a/app/views/api/v1/accounts/conversations/attachments.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/attachments.json.jbuilder
@@ -3,6 +3,7 @@ json.meta do
end
json.payload @attachments do |attachment|
+ json.id attachment.push_event_data[:id]
json.message_id attachment.push_event_data[:message_id]
json.thumb_url attachment.push_event_data[:thumb_url]
json.data_url attachment.push_event_data[:data_url]
diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
index e007f4900..19d080b47 100644
--- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb
@@ -1039,6 +1039,8 @@ RSpec.describe 'Conversations API', type: :request do
expect(response).to have_http_status(:success)
response_body = response.parsed_body
+ attachment = conversation.messages.last.attachments.first
+ expect(response_body['payload'].first['id']).to eq(attachment.id)
expect(response_body['payload'].first['file_type']).to eq('image')
expect(response_body['payload'].first['sender']['id']).to eq(conversation.messages.last.sender.id)
end