feat: Add channel-specific file upload rules and size limits (#12237)

This commit is contained in:
Sivin Varghese 2025-08-26 22:23:39 +05:30 committed by GitHub
parent 19faa7fdfa
commit 39dfa35229
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 168 additions and 150 deletions

View File

@ -6,15 +6,11 @@ import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage'; import * as ActiveStorage from 'activestorage';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { import { getAllowedFileTypesByChannel } from '@chatwoot/utils';
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
ALLOWED_FILE_TYPES_FOR_LINE,
ALLOWED_FILE_TYPES_FOR_INSTAGRAM,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton.vue'; import VideoCallButton from '../VideoCallButton.vue';
import AIAssistanceButton from '../AIAssistanceButton.vue'; import AIAssistanceButton from '../AIAssistanceButton.vue';
import { REPLY_EDITOR_MODES } from './constants'; import { REPLY_EDITOR_MODES } from './constants';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue'; import NextButton from 'dashboard/components-next/button/Button.vue';
@ -196,17 +192,16 @@ export default {
return this.conversationType === 'instagram_direct_message'; return this.conversationType === 'instagram_direct_message';
}, },
allowedFileTypes() { allowedFileTypes() {
if (this.isATwilioWhatsAppChannel) { let channelType = this.channelType || this.inbox?.channel_type;
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
}
if (this.isALineChannel) {
return ALLOWED_FILE_TYPES_FOR_LINE;
}
if (this.isAnInstagramChannel || this.isInstagramDM) { 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() { enableDragAndDrop() {
return !this.newConversationModalActive; return !this.newConversationModalActive;

View File

@ -4,7 +4,7 @@ import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { DirectUpload } from 'activestorage'; import { DirectUpload } from 'activestorage';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; 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/store');
vi.mock('dashboard/composables', () => ({ vi.mock('dashboard/composables', () => ({
@ -13,6 +13,7 @@ vi.mock('dashboard/composables', () => ({
vi.mock('vue-i18n'); vi.mock('vue-i18n');
vi.mock('activestorage'); vi.mock('activestorage');
vi.mock('shared/helpers/FileHelper'); vi.mock('shared/helpers/FileHelper');
vi.mock('@chatwoot/utils');
describe('useFileUpload', () => { describe('useFileUpload', () => {
const mockAttachFile = vi.fn(); const mockAttachFile = vi.fn();
@ -22,6 +23,11 @@ describe('useFileUpload', () => {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }), file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
}; };
const inbox = {
channel_type: 'Channel::WhatsApp',
medium: 'whatsapp',
};
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -37,11 +43,12 @@ describe('useFileUpload', () => {
useI18n.mockReturnValue({ t: mockTranslate }); useI18n.mockReturnValue({ t: mockTranslate });
checkFileSizeLimit.mockReturnValue(true); 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({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: false, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
@ -52,6 +59,16 @@ describe('useFileUpload', () => {
onFileUpload(mockFile); 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( expect(DirectUpload).toHaveBeenCalledWith(
mockFile.file, mockFile.file,
'/api/v1/accounts/123/conversations/456/direct_uploads', '/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 => { useMapGetter.mockImplementation(getter => {
const getterMap = { const getterMap = {
getCurrentAccountId: { value: '123' }, getCurrentAccountId: { value: '123' },
@ -75,22 +92,24 @@ describe('useFileUpload', () => {
}); });
const { onFileUpload } = useFileUpload({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: false, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
onFileUpload(mockFile); onFileUpload(mockFile);
expect(DirectUpload).not.toHaveBeenCalled(); expect(DirectUpload).not.toHaveBeenCalled();
expect(getMaxUploadSizeByChannel).toHaveBeenCalled();
expect(checkFileSizeLimit).toHaveBeenCalledWith(mockFile, 25);
expect(mockAttachFile).toHaveBeenCalledWith({ file: mockFile }); 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); checkFileSizeLimit.mockReturnValue(false);
mockTranslate.mockReturnValue('File size exceeds limit'); mockTranslate.mockReturnValue('File size exceeds limit');
const { onFileUpload } = useFileUpload({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: false, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
@ -100,28 +119,37 @@ describe('useFileUpload', () => {
expect(mockAttachFile).not.toHaveBeenCalled(); 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({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: true, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
DirectUpload.mockImplementation(() => ({
create: cb => cb(null, { signed_id: 'blob' }),
}));
onFileUpload(mockFile); onFileUpload(mockFile);
expect(checkFileSizeLimit).toHaveBeenCalledWith( expect(getMaxUploadSizeByChannel).toHaveBeenCalledWith({
mockFile, channelType: inbox.channel_type,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL 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'; const mockError = 'Upload failed';
DirectUpload.mockImplementation(() => ({ DirectUpload.mockImplementation(() => ({
create: callback => callback(mockError, null), create: callback => callback(mockError, null),
})); }));
const { onFileUpload } = useFileUpload({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: false, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
@ -131,15 +159,16 @@ describe('useFileUpload', () => {
expect(mockAttachFile).not.toHaveBeenCalled(); expect(mockAttachFile).not.toHaveBeenCalled();
}); });
it('should do nothing when file is null', () => { it('does nothing when file is null', () => {
const { onFileUpload } = useFileUpload({ const { onFileUpload } = useFileUpload({
isATwilioSMSChannel: false, inbox,
attachFile: mockAttachFile, attachFile: mockAttachFile,
}); });
onFileUpload(null); onFileUpload(null);
expect(checkFileSizeLimit).not.toHaveBeenCalled(); expect(checkFileSizeLimit).not.toHaveBeenCalled();
expect(getMaxUploadSizeByChannel).not.toHaveBeenCalled();
expect(mockAttachFile).not.toHaveBeenCalled(); expect(mockAttachFile).not.toHaveBeenCalled();
expect(useAlert).not.toHaveBeenCalled(); expect(useAlert).not.toHaveBeenCalled();
}); });

View File

@ -1,22 +1,17 @@
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { DirectUpload } from 'activestorage'; 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 { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
/** /**
* Composable for handling file uploads in conversations * Composable for handling file uploads in conversations
* @param {Object} options - Configuration options * @param {Object} options
* @param {boolean} options.isATwilioSMSChannel - Whether the current channel is Twilio SMS * @param {Object} options.inbox - Current inbox object (has channel_type, medium, etc.)
* @param {Function} options.attachFile - Callback function to handle file attachment * @param {Function} options.attachFile - Callback to handle file attachment
* @returns {Object} File upload methods and utilities
*/ */
export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => { export const useFileUpload = ({ inbox, attachFile }) => {
const { t } = useI18n(); const { t } = useI18n();
const accountId = useMapGetter('getCurrentAccountId'); const accountId = useMapGetter('getCurrentAccountId');
@ -24,57 +19,66 @@ export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => {
const currentChat = useMapGetter('getSelectedChat'); const currentChat = useMapGetter('getSelectedChat');
const globalConfig = useMapGetter('globalConfig/get'); const globalConfig = useMapGetter('globalConfig/get');
const maxFileSize = computed(() => // helper: compute max upload size for a given file's mime
isATwilioSMSChannel const maxSizeFor = mime =>
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL getMaxUploadSizeByChannel({
: MAXIMUM_FILE_UPLOAD_SIZE 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 => { const handleDirectFileUpload = file => {
if (!file) return; if (!file) return;
if (checkFileSizeLimit(file, maxFileSize.value)) { const mime = file.file?.type || file.type;
const upload = new DirectUpload( const maxSizeMB = maxSizeFor(mime);
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 (!checkFileSizeLimit(file, maxSizeMB)) {
if (error) { alertOverLimit(maxSizeMB);
useAlert(error); return;
} else {
attachFile({ file, blob });
}
});
} else {
useAlert(
t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: 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
);
},
}
);
upload.create((error, blob) => {
if (error) {
useAlert(error);
} else {
attachFile({ file, blob });
}
});
}; };
const handleIndirectFileUpload = file => { const handleIndirectFileUpload = file => {
if (!file) return; if (!file) return;
if (checkFileSizeLimit(file, maxFileSize.value)) { const mime = file.file?.type || file.type;
attachFile({ file }); const maxSizeMB = maxSizeFor(mime);
} else {
useAlert( if (!checkFileSizeLimit(file, maxSizeMB)) {
t('CONVERSATION.FILE_SIZE_LIMIT', { alertOverLimit(maxSizeMB);
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE: maxFileSize.value, return;
})
);
} }
attachFile({ file });
}; };
const onFileUpload = file => { const onFileUpload = file => {
@ -85,7 +89,5 @@ export const useFileUpload = ({ isATwilioSMSChannel, attachFile }) => {
} }
}; };
return { return { onFileUpload };
onFileUpload,
};
}; };

View File

@ -1,10 +1,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; 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 { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { getMaxUploadSizeByChannel } from '@chatwoot/utils';
import { DirectUpload } from 'activestorage'; import { DirectUpload } from 'activestorage';
export default { export default {
@ -13,7 +10,22 @@ export default {
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
}), }),
}, },
methods: { 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) { onFileUpload(file) {
if (this.globalConfig.directUploadsEnabled) { if (this.globalConfig.directUploadsEnabled) {
this.onDirectFileUpload(file); this.onDirectFileUpload(file);
@ -21,59 +33,52 @@ export default {
this.onIndirectFileUpload(file); this.onIndirectFileUpload(file);
} }
}, },
onDirectFileUpload(file) { onDirectFileUpload(file) {
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel if (!file) return;
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
: MAXIMUM_FILE_UPLOAD_SIZE;
if (!file) { const mime = file.file?.type || file.type;
const maxSizeMB = this.maxSizeFor(mime);
if (!checkFileSizeLimit(file, maxSizeMB)) {
this.alertOverLimit(maxSizeMB);
return; 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) => { const upload = new DirectUpload(
if (error) { file.file,
useAlert(error); `/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`,
} else { {
this.attachFile({ file, blob }); directUploadWillCreateBlobWithXHR: xhr => {
} xhr.setRequestHeader(
}); 'api_access_token',
} else { this.currentUser.access_token
useAlert( );
this.$t('CONVERSATION.FILE_SIZE_LIMIT', { },
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE, }
}) );
);
} upload.create((error, blob) => {
if (error) {
useAlert(error);
} else {
this.attachFile({ file, blob });
}
});
}, },
onIndirectFileUpload(file) { onIndirectFileUpload(file) {
const MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE = this.isATwilioSMSChannel if (!file) return;
? MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL
: MAXIMUM_FILE_UPLOAD_SIZE; const mime = file.file?.type || file.type;
if (!file) { const maxSizeMB = this.maxSizeFor(mime);
if (!checkFileSizeLimit(file, maxSizeMB)) {
this.alertOverLimit(maxSizeMB);
return; return;
} }
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
this.attachFile({ file }); this.attachFile({ file });
} else {
useAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
})
);
}
}, },
}, },
}; };

View File

@ -36,7 +36,6 @@ export const CONVERSATION_PRIORITY_ORDER = {
// Size in mega bytes // Size in mega bytes
export const MAXIMUM_FILE_UPLOAD_SIZE = 40; export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5;
export const ALLOWED_FILE_TYPES = export const ALLOWED_FILE_TYPES =
'image/*,' + '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.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' +
'application/vnd.openxmlformats-officedocument.wordprocessingml.document,'; '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 = [ export const CSAT_RATINGS = [
{ {
key: 'disappointed', key: 'disappointed',

View File

@ -34,7 +34,7 @@
"@breezystack/lamejs": "^1.2.7", "@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3", "@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.2.1", "@chatwoot/prosemirror-schema": "1.2.1",
"@chatwoot/utils": "^0.0.49", "@chatwoot/utils": "^0.0.50",
"@formkit/core": "^1.6.7", "@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7", "@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",

10
pnpm-lock.yaml generated
View File

@ -23,8 +23,8 @@ importers:
specifier: 1.2.1 specifier: 1.2.1
version: 1.2.1 version: 1.2.1
'@chatwoot/utils': '@chatwoot/utils':
specifier: ^0.0.49 specifier: ^0.0.50
version: 0.0.49 version: 0.0.50
'@formkit/core': '@formkit/core':
specifier: ^1.6.7 specifier: ^1.6.7
version: 1.6.7 version: 1.6.7
@ -406,8 +406,8 @@ packages:
'@chatwoot/prosemirror-schema@1.2.1': '@chatwoot/prosemirror-schema@1.2.1':
resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==} resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==}
'@chatwoot/utils@0.0.49': '@chatwoot/utils@0.0.50':
resolution: {integrity: sha512-Co68VzaFtctTNYKY6y4izBBATvk6/8ZVtkyEP5HL72uhFDA11LrY5pqSh04HMoFyfdIU+uVPimfI45HAeso1IA==} resolution: {integrity: sha512-GGvB+ujt+8qnV6KKEM2IH9/JmbMpMMfrJ4C+SdPvd/WbhUEFvRof0D9fsU+444G8BUh2om7GM7mXOa3pEH+Vtw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@codemirror/commands@6.7.0': '@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-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
prosemirror-view: 1.34.1 prosemirror-view: 1.34.1
'@chatwoot/utils@0.0.49': '@chatwoot/utils@0.0.50':
dependencies: dependencies:
date-fns: 2.30.0 date-fns: 2.30.0