diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 279686ced..4802fc46f 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -6,15 +6,11 @@ import FileUpload from 'vue-upload-component'; import * as ActiveStorage from 'activestorage'; import inboxMixin from 'shared/mixins/inboxMixin'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; -import { - ALLOWED_FILE_TYPES, - ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP, - ALLOWED_FILE_TYPES_FOR_LINE, - ALLOWED_FILE_TYPES_FOR_INSTAGRAM, -} from 'shared/constants/messages'; +import { getAllowedFileTypesByChannel } from '@chatwoot/utils'; import VideoCallButton from '../VideoCallButton.vue'; import AIAssistanceButton from '../AIAssistanceButton.vue'; import { REPLY_EDITOR_MODES } from './constants'; +import { INBOX_TYPES } from 'dashboard/helper/inbox'; import { mapGetters } from 'vuex'; import NextButton from 'dashboard/components-next/button/Button.vue'; @@ -196,17 +192,16 @@ export default { return this.conversationType === 'instagram_direct_message'; }, allowedFileTypes() { - if (this.isATwilioWhatsAppChannel) { - return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP; - } - if (this.isALineChannel) { - return ALLOWED_FILE_TYPES_FOR_LINE; - } + let channelType = this.channelType || this.inbox?.channel_type; + if (this.isAnInstagramChannel || this.isInstagramDM) { - return ALLOWED_FILE_TYPES_FOR_INSTAGRAM; + channelType = INBOX_TYPES.INSTAGRAM; } - return ALLOWED_FILE_TYPES; + return getAllowedFileTypesByChannel({ + channelType, + medium: this.inbox?.medium, + }); }, enableDragAndDrop() { return !this.newConversationModalActive; diff --git a/app/javascript/dashboard/composables/spec/useFileUpload.spec.js b/app/javascript/dashboard/composables/spec/useFileUpload.spec.js index 27e17dc43..968a1b057 100644 --- a/app/javascript/dashboard/composables/spec/useFileUpload.spec.js +++ b/app/javascript/dashboard/composables/spec/useFileUpload.spec.js @@ -4,7 +4,7 @@ import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; import { DirectUpload } from 'activestorage'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; -import { MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL } from 'shared/constants/messages'; +import { getMaxUploadSizeByChannel } from '@chatwoot/utils'; vi.mock('dashboard/composables/store'); vi.mock('dashboard/composables', () => ({ @@ -13,6 +13,7 @@ vi.mock('dashboard/composables', () => ({ vi.mock('vue-i18n'); vi.mock('activestorage'); vi.mock('shared/helpers/FileHelper'); +vi.mock('@chatwoot/utils'); describe('useFileUpload', () => { const mockAttachFile = vi.fn(); @@ -22,6 +23,11 @@ describe('useFileUpload', () => { file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }), }; + const inbox = { + channel_type: 'Channel::WhatsApp', + medium: 'whatsapp', + }; + beforeEach(() => { vi.clearAllMocks(); @@ -37,11 +43,12 @@ describe('useFileUpload', () => { useI18n.mockReturnValue({ t: mockTranslate }); checkFileSizeLimit.mockReturnValue(true); + getMaxUploadSizeByChannel.mockReturnValue(25); // default max size MB for tests }); - it('should handle direct file upload when enabled', () => { + it('handles direct file upload when direct uploads enabled', () => { const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: false, + inbox, attachFile: mockAttachFile, }); @@ -52,6 +59,16 @@ describe('useFileUpload', () => { onFileUpload(mockFile); + // size rules called with inbox + mime + expect(getMaxUploadSizeByChannel).toHaveBeenCalledWith({ + channelType: inbox.channel_type, + medium: inbox.medium, + mime: 'image/jpeg', + }); + + // size check called with max from helper + expect(checkFileSizeLimit).toHaveBeenCalledWith(mockFile, 25); + expect(DirectUpload).toHaveBeenCalledWith( mockFile.file, '/api/v1/accounts/123/conversations/456/direct_uploads', @@ -63,7 +80,7 @@ describe('useFileUpload', () => { }); }); - it('should handle indirect file upload when direct upload is disabled', () => { + it('handles indirect file upload when direct upload disabled', () => { useMapGetter.mockImplementation(getter => { const getterMap = { getCurrentAccountId: { value: '123' }, @@ -75,22 +92,24 @@ describe('useFileUpload', () => { }); const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: false, + inbox, attachFile: mockAttachFile, }); onFileUpload(mockFile); expect(DirectUpload).not.toHaveBeenCalled(); + expect(getMaxUploadSizeByChannel).toHaveBeenCalled(); + expect(checkFileSizeLimit).toHaveBeenCalledWith(mockFile, 25); expect(mockAttachFile).toHaveBeenCalledWith({ file: mockFile }); }); - it('should show alert when file size exceeds limit', () => { + it('shows alert when file size exceeds limit', () => { checkFileSizeLimit.mockReturnValue(false); mockTranslate.mockReturnValue('File size exceeds limit'); const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: false, + inbox, attachFile: mockAttachFile, }); @@ -100,28 +119,37 @@ describe('useFileUpload', () => { expect(mockAttachFile).not.toHaveBeenCalled(); }); - it('should use different max file size for Twilio SMS channel', () => { + it('uses per-mime limits from helper', () => { + getMaxUploadSizeByChannel.mockImplementation(({ mime }) => + mime.startsWith('image/') ? 10 : 50 + ); const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: true, + inbox, attachFile: mockAttachFile, }); + DirectUpload.mockImplementation(() => ({ + create: cb => cb(null, { signed_id: 'blob' }), + })); + onFileUpload(mockFile); - expect(checkFileSizeLimit).toHaveBeenCalledWith( - mockFile, - MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL - ); + expect(getMaxUploadSizeByChannel).toHaveBeenCalledWith({ + channelType: inbox.channel_type, + medium: inbox.medium, + mime: 'image/jpeg', + }); + expect(checkFileSizeLimit).toHaveBeenCalledWith(mockFile, 10); }); - it('should handle direct upload errors', () => { + it('handles direct upload errors', () => { const mockError = 'Upload failed'; DirectUpload.mockImplementation(() => ({ create: callback => callback(mockError, null), })); const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: false, + inbox, attachFile: mockAttachFile, }); @@ -131,15 +159,16 @@ describe('useFileUpload', () => { expect(mockAttachFile).not.toHaveBeenCalled(); }); - it('should do nothing when file is null', () => { + it('does nothing when file is null', () => { const { onFileUpload } = useFileUpload({ - isATwilioSMSChannel: false, + inbox, attachFile: mockAttachFile, }); onFileUpload(null); expect(checkFileSizeLimit).not.toHaveBeenCalled(); + expect(getMaxUploadSizeByChannel).not.toHaveBeenCalled(); expect(mockAttachFile).not.toHaveBeenCalled(); expect(useAlert).not.toHaveBeenCalled(); }); diff --git a/app/javascript/dashboard/composables/useFileUpload.js b/app/javascript/dashboard/composables/useFileUpload.js index dd6779250..423462fda 100644 --- a/app/javascript/dashboard/composables/useFileUpload.js +++ b/app/javascript/dashboard/composables/useFileUpload.js @@ -1,22 +1,17 @@ -import { computed } from 'vue'; import { useMapGetter } from 'dashboard/composables/store'; import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; import { DirectUpload } from 'activestorage'; -import { - MAXIMUM_FILE_UPLOAD_SIZE, - MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL, -} from 'shared/constants/messages'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; +import { getMaxUploadSizeByChannel } from '@chatwoot/utils'; /** * Composable for handling file uploads in conversations - * @param {Object} options - Configuration options - * @param {boolean} options.isATwilioSMSChannel - Whether the current channel is Twilio SMS - * @param {Function} options.attachFile - Callback function to handle file attachment - * @returns {Object} File upload methods and utilities + * @param {Object} options + * @param {Object} options.inbox - Current inbox object (has channel_type, medium, etc.) + * @param {Function} options.attachFile - Callback to handle file attachment */ -export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => { +export const useFileUpload = ({ inbox, attachFile }) => { const { t } = useI18n(); const accountId = useMapGetter('getCurrentAccountId'); @@ -24,57 +19,66 @@ export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => { const currentChat = useMapGetter('getSelectedChat'); const globalConfig = useMapGetter('globalConfig/get'); - const maxFileSize = computed(() => - isATwilioSMSChannel - ? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL - : MAXIMUM_FILE_UPLOAD_SIZE - ); + // helper: compute max upload size for a given file's mime + const maxSizeFor = mime => + getMaxUploadSizeByChannel({ + channelType: inbox?.channel_type, + medium: inbox?.medium, // e.g. 'sms' | 'whatsapp' | etc. + mime, // e.g. 'image/png' + }); + + const alertOverLimit = maxSizeMB => + useAlert( + t('CONVERSATION.FILE_SIZE_LIMIT', { + MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: maxSizeMB, + }) + ); const handleDirectFileUpload = file => { if (!file) return; - if (checkFileSizeLimit(file, maxFileSize.value)) { - const upload = new DirectUpload( - file.file, - `/api/v1/accounts/${accountId.value}/conversations/${currentChat.value.id}/direct_uploads`, - { - directUploadWillCreateBlobWithXHR: xhr => { - xhr.setRequestHeader( - 'api_access_token', - currentUser.value.access_token - ); - }, - } - ); + const mime = file.file?.type || file.type; + const maxSizeMB = maxSizeFor(mime); - upload.create((error, blob) => { - if (error) { - useAlert(error); - } else { - attachFile({ file, blob }); - } - }); - } else { - useAlert( - t('CONVERSATION.FILE_SIZE_LIMIT', { - MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: maxFileSize.value, - }) - ); + if (!checkFileSizeLimit(file, maxSizeMB)) { + alertOverLimit(maxSizeMB); + return; } + + const upload = new DirectUpload( + file.file, + `/api/v1/accounts/${accountId.value}/conversations/${currentChat.value.id}/direct_uploads`, + { + directUploadWillCreateBlobWithXHR: xhr => { + xhr.setRequestHeader( + 'api_access_token', + currentUser.value.access_token + ); + }, + } + ); + + upload.create((error, blob) => { + if (error) { + useAlert(error); + } else { + attachFile({ file, blob }); + } + }); }; const handleIndirectFileUpload = file => { if (!file) return; - if (checkFileSizeLimit(file, maxFileSize.value)) { - attachFile({ file }); - } else { - useAlert( - t('CONVERSATION.FILE_SIZE_LIMIT', { - MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: maxFileSize.value, - }) - ); + const mime = file.file?.type || file.type; + const maxSizeMB = maxSizeFor(mime); + + if (!checkFileSizeLimit(file, maxSizeMB)) { + alertOverLimit(maxSizeMB); + return; } + + attachFile({ file }); }; const onFileUpload = file => { @@ -85,7 +89,5 @@ export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => { } }; - return { - onFileUpload, - }; + return { onFileUpload }; }; diff --git a/app/javascript/dashboard/mixins/fileUploadMixin.js b/app/javascript/dashboard/mixins/fileUploadMixin.js index b5313f27c..85d640ec5 100644 --- a/app/javascript/dashboard/mixins/fileUploadMixin.js +++ b/app/javascript/dashboard/mixins/fileUploadMixin.js @@ -1,10 +1,7 @@ import { mapGetters } from 'vuex'; import { useAlert } from 'dashboard/composables'; -import { - MAXIMUM_FILE_UPLOAD_SIZE, - MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL, -} from 'shared/constants/messages'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; +import { getMaxUploadSizeByChannel } from '@chatwoot/utils'; import { DirectUpload } from 'activestorage'; export default { @@ -13,7 +10,22 @@ export default { accountId: 'getCurrentAccountId', }), }, + methods: { + maxSizeFor(mime) { + return getMaxUploadSizeByChannel({ + channelType: this.inbox?.channel_type, + medium: this.inbox?.medium, // e.g. 'sms' | 'whatsapp' + mime, // e.g. 'image/png' + }); + }, + alertOverLimit(maxSizeMB) { + useAlert( + this.$t('CONVERSATION.FILE_SIZE_LIMIT', { + MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: maxSizeMB, + }) + ); + }, onFileUpload(file) { if (this.globalConfig.directUploadsEnabled) { this.onDirectFileUpload(file); @@ -21,59 +33,52 @@ export default { this.onIndirectFileUpload(file); } }, + onDirectFileUpload(file) { - const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel - ? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL - : MAXIMUM_FILE_UPLOAD_SIZE; + if (!file) return; - if (!file) { + const mime = file.file?.type || file.type; + const maxSizeMB = this.maxSizeFor(mime); + + if (!checkFileSizeLimit(file, maxSizeMB)) { + this.alertOverLimit(maxSizeMB); return; } - if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) { - const upload = new DirectUpload( - file.file, - `/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`, - { - directUploadWillCreateBlobWithXHR: xhr => { - xhr.setRequestHeader( - 'api_access_token', - this.currentUser.access_token - ); - }, - } - ); - upload.create((error, blob) => { - if (error) { - useAlert(error); - } else { - this.attachFile({ file, blob }); - } - }); - } else { - useAlert( - this.$t('CONVERSATION.FILE_SIZE_LIMIT', { - MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE, - }) - ); - } + const upload = new DirectUpload( + file.file, + `/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`, + { + directUploadWillCreateBlobWithXHR: xhr => { + xhr.setRequestHeader( + 'api_access_token', + this.currentUser.access_token + ); + }, + } + ); + + upload.create((error, blob) => { + if (error) { + useAlert(error); + } else { + this.attachFile({ file, blob }); + } + }); }, + onIndirectFileUpload(file) { - const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel - ? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL - : MAXIMUM_FILE_UPLOAD_SIZE; - if (!file) { + if (!file) return; + + const mime = file.file?.type || file.type; + const maxSizeMB = this.maxSizeFor(mime); + + if (!checkFileSizeLimit(file, maxSizeMB)) { + this.alertOverLimit(maxSizeMB); return; } - if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) { - this.attachFile({ file }); - } else { - useAlert( - this.$t('CONVERSATION.FILE_SIZE_LIMIT', { - MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE, - }) - ); - } + + this.attachFile({ file }); }, }, }; diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index 7b7b4f331..d87db470d 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -36,7 +36,6 @@ export const CONVERSATION_PRIORITY_ORDER = { // Size in mega bytes export const MAXIMUM_FILE_UPLOAD_SIZE = 40; -export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5; export const ALLOWED_FILE_TYPES = 'image/*,' + @@ -50,18 +49,6 @@ export const ALLOWED_FILE_TYPES = 'application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document,'; -export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP = - 'image/png, image/jpeg,' + - 'audio/mpeg, audio/opus, audio/ogg, audio/amr,' + - 'video/mp4,' + - 'application/pdf,'; -// https://developers.line.biz/en/reference/messaging-api/#image-message, https://developers.line.biz/en/reference/messaging-api/#video-message -export const ALLOWED_FILE_TYPES_FOR_LINE = 'image/png, image/jpeg,video/mp4'; - -// https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api#requirements -export const ALLOWED_FILE_TYPES_FOR_INSTAGRAM = - 'image/png, image/jpeg, video/mp4, video/mov, video/webm'; - export const CSAT_RATINGS = [ { key: 'disappointed', diff --git a/package.json b/package.json index 31d07021e..914bb784f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.2.1", - "@chatwoot/utils": "^0.0.49", + "@chatwoot/utils": "^0.0.50", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca55fbe34..5babe7989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.2.1 version: 1.2.1 '@chatwoot/utils': - specifier: ^0.0.49 - version: 0.0.49 + specifier: ^0.0.50 + version: 0.0.50 '@formkit/core': specifier: ^1.6.7 version: 1.6.7 @@ -406,8 +406,8 @@ packages: '@chatwoot/prosemirror-schema@1.2.1': resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==} - '@chatwoot/utils@0.0.49': - resolution: {integrity: sha512-Co68VzaFtctTNYKY6y4izBBATvk6/8ZVtkyEP5HL72uhFDA11LrY5pqSh04HMoFyfdIU+uVPimfI45HAeso1IA==} + '@chatwoot/utils@0.0.50': + resolution: {integrity: sha512-GGvB+ujt+8qnV6KKEM2IH9/JmbMpMMfrJ4C+SdPvd/WbhUEFvRof0D9fsU+444G8BUh2om7GM7mXOa3pEH+Vtw==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -5268,7 +5268,7 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.49': + '@chatwoot/utils@0.0.50': dependencies: date-fns: 2.30.0