From e1bde016a33195e8a1dadee8e2da7d25d075a77c Mon Sep 17 00:00:00 2001 From: Zita Szupera Date: Wed, 9 Oct 2024 11:22:38 -0500 Subject: [PATCH] fix: add error message if attachment limit is exceeded --- .../stream-chat-angular/src/assets/i18n/en.ts | 4 + .../src/lib/attachment.service.spec.ts | 97 +++++++++++++++++++ .../src/lib/attachment.service.ts | 73 +++++++++++++- .../message-input.component.html | 10 +- .../message-input.component.spec.ts | 36 +++++-- 5 files changed, 212 insertions(+), 8 deletions(-) diff --git a/projects/stream-chat-angular/src/assets/i18n/en.ts b/projects/stream-chat-angular/src/assets/i18n/en.ts index 8c51242b..68c6fb15 100644 --- a/projects/stream-chat-angular/src/assets/i18n/en.ts +++ b/projects/stream-chat-angular/src/assets/i18n/en.ts @@ -126,5 +126,9 @@ export const en = { 'An error has occurred during recording': 'An error has occurred during recording', 'Media recording not supported': 'Media recording not supported', + "You can't uplod more than {{max}} attachments": + "You can't uplod more than {{max}} attachments", + 'You currently have {{count}} attachments, the maximum is {{max}}': + 'You currently have {{count}} attachments, the maximum is {{max}}', }, }; diff --git a/projects/stream-chat-angular/src/lib/attachment.service.spec.ts b/projects/stream-chat-angular/src/lib/attachment.service.spec.ts index 57411343..38f2a070 100644 --- a/projects/stream-chat-angular/src/lib/attachment.service.spec.ts +++ b/projects/stream-chat-angular/src/lib/attachment.service.spec.ts @@ -202,6 +202,100 @@ describe('AttachmentService', () => { ); }); + it('should display error message if attachment limit is exceeded', () => { + const notificationService = TestBed.inject(NotificationService); + spyOn(notificationService, 'addPermanentNotification'); + const attachments = new Array(30) + .fill(null) + .map(() => ({ type: 'custom' })); + service.customAttachments$.next(attachments); + + expect(notificationService.addPermanentNotification).not.toHaveBeenCalled(); + + service.customAttachments$.next([ + ...service.customAttachments$.value, + { type: 'custom' }, + ]); + + expect(notificationService.addPermanentNotification).toHaveBeenCalledWith( + 'streamChat.You currently have {{count}} attachments, the maximum is {{max}}', + 'error', + { count: 31, max: service.maxNumberOfAttachments } + ); + }); + + it('should prevent file upload if limit would be exceeded', async () => { + const notificationService = TestBed.inject(NotificationService); + spyOn(notificationService, 'addTemporaryNotification'); + const attachments = new Array(28) + .fill(null) + .map(() => ({ type: 'custom' })); + service.customAttachments$.next(attachments); + + expect(notificationService.addTemporaryNotification).not.toHaveBeenCalled(); + + const files = new Array(3) + .fill(null) + .map(() => ({ name: 'my_image.png', type: 'image/png' } as File)); + await service.filesSelected(files); + + expect(notificationService.addTemporaryNotification).toHaveBeenCalledWith( + `streamChat.You can't uplod more than {{max}} attachments`, + 'error', + undefined, + { max: service.maxNumberOfAttachments } + ); + + expect(uploadAttachmentsSpy).not.toHaveBeenCalled(); + }); + + it('should prevent voice recording upload if limit be exceeded', async () => { + const notificationService = TestBed.inject(NotificationService); + spyOn(notificationService, 'addTemporaryNotification'); + const attachments = new Array(29) + .fill(null) + .map(() => ({ type: 'custom' })); + service.customAttachments$.next(attachments); + + expect(notificationService.addTemporaryNotification).not.toHaveBeenCalled(); + + const voiceRecording = { + recording: { name: 'test', type: 'audio/wav' } as File, + asset_url: 'test', + duration: 3.073, + file_size: 96044, + mime_type: 'audio/wav', + waveform_data: [ + 0, 0, 0.1931696001580504, 0.1931696001580504, 0.2430087421276868, + 0.2430087421276868, 0.2531576820785543, + ], + }; + + uploadAttachmentsSpy.and.resolveTo([ + { + file: voiceRecording.recording, + state: 'success', + type: 'voiceRecording', + }, + ]); + + await service.uploadVoiceRecording(voiceRecording); + + expect(notificationService.addTemporaryNotification).not.toHaveBeenCalled(); + + uploadAttachmentsSpy.calls.reset(); + await service.uploadVoiceRecording(voiceRecording); + + expect(notificationService.addTemporaryNotification).toHaveBeenCalledWith( + `streamChat.You can't uplod more than {{max}} attachments`, + 'error', + undefined, + { max: service.maxNumberOfAttachments } + ); + + expect(uploadAttachmentsSpy).not.toHaveBeenCalled(); + }); + it('should retry attachment upload', async () => { const file = { name: 'my_image.png', type: 'image/png' } as File; uploadAttachmentsSpy.and.resolveTo([ @@ -381,6 +475,8 @@ describe('AttachmentService', () => { })); it('should reset attachments', () => { + const errorNotificationHideSpy = jasmine.createSpy(); + service['attachmentLimitNotificationHide'] = errorNotificationHideSpy; const spy = jasmine.createSpy(); service.customAttachments$.next([{ type: 'custom' }]); service.attachmentUploads$.subscribe(spy); @@ -389,6 +485,7 @@ describe('AttachmentService', () => { expect(spy).toHaveBeenCalledWith([]); expect(service.customAttachments$.value).toEqual([]); + expect(errorNotificationHideSpy).toHaveBeenCalledWith(); }); it('should map to attachments', async () => { diff --git a/projects/stream-chat-angular/src/lib/attachment.service.ts b/projects/stream-chat-angular/src/lib/attachment.service.ts index 25ac3ad1..fd0c7caa 100644 --- a/projects/stream-chat-angular/src/lib/attachment.service.ts +++ b/projects/stream-chat-angular/src/lib/attachment.service.ts @@ -1,6 +1,13 @@ import { Injectable } from '@angular/core'; import { createUriFromBlob, isImageFile } from './file-utils'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + map, + Observable, + shareReplay, + take, +} from 'rxjs'; import { AppSettings, Attachment } from 'stream-chat'; import { ChannelService } from './channel.service'; import { isImageAttachment } from './is-image-attachment'; @@ -42,10 +49,21 @@ export class AttachmentService< * By default the SDK components won't display these, but you can provide your own `customAttachmentPreviewListTemplate$` and `customAttachmentListTemplate$` for the [`CustomTemplatesService`](../../services/CustomTemplatesService). */ customAttachments$ = new BehaviorSubject[]>([]); + /** + * The current number of attachments + */ + attachmentsCounter$: Observable; + /** + * The maximum number of attachments allowed for a message. + * + * The maximum is 30, you can set it to lower, but not higher. + */ + maxNumberOfAttachments = 30; private attachmentUploadsSubject = new BehaviorSubject( [] ); private appSettings: AppSettings | undefined; + private attachmentLimitNotificationHide?: () => void; constructor( private channelService: ChannelService, @@ -57,6 +75,30 @@ export class AttachmentService< this.chatClientService.appSettings$.subscribe( (appSettings) => (this.appSettings = appSettings) ); + this.attachmentsCounter$ = combineLatest([ + this.attachmentUploads$, + this.customAttachments$, + ]).pipe( + map(([attchmentUploads, customAttachments]) => { + return ( + attchmentUploads.filter((u) => u.state === 'success').length + + customAttachments.length + ); + }), + shareReplay(1) + ); + this.attachmentsCounter$.subscribe((count) => { + if (count > this.maxNumberOfAttachments) { + this.attachmentLimitNotificationHide = + this.notificationService.addPermanentNotification( + 'streamChat.You currently have {{count}} attachments, the maximum is {{max}}', + 'error', + { count, max: this.maxNumberOfAttachments } + ); + } else { + this.attachmentLimitNotificationHide?.(); + } + }); } /** @@ -65,6 +107,7 @@ export class AttachmentService< resetAttachmentUploads() { this.attachmentUploadsSubject.next([]); this.customAttachments$.next([]); + this.attachmentLimitNotificationHide?.(); } /** @@ -73,6 +116,9 @@ export class AttachmentService< * @returns A promise with true or false. If false is returned the upload was canceled because of a client side error. The error is emitted via the `NotificationService`. */ async uploadVoiceRecording(audioRecording: AudioRecording) { + if (!this.isWithinLimit(1)) { + return false; + } if ( !(await this.areAttachmentsHaveValidExtension([audioRecording.recording])) ) { @@ -112,6 +158,10 @@ export class AttachmentService< const files = Array.from(fileList); + if (!this.isWithinLimit(files.length)) { + return false; + } + if (!(await this.areAttachmentsHaveValidExtension(files))) { return false; } @@ -501,4 +551,25 @@ export class AttachmentService< }); return isValid; } + + private isWithinLimit(numberOfNewAttachments: number) { + let currentNumberOfAttachments: number = 0; + this.attachmentsCounter$ + .pipe(take(1)) + .subscribe((counter) => (currentNumberOfAttachments = counter)); + if ( + currentNumberOfAttachments + numberOfNewAttachments > + this.maxNumberOfAttachments + ) { + this.notificationService.addTemporaryNotification( + `streamChat.You can't uplod more than {{max}} attachments`, + 'error', + undefined, + { max: this.maxNumberOfAttachments } + ); + return false; + } else { + return true; + } + } } diff --git a/projects/stream-chat-angular/src/lib/message-input/message-input.component.html b/projects/stream-chat-angular/src/lib/message-input/message-input.component.html index fff7c1e8..784efc98 100644 --- a/projects/stream-chat-angular/src/lib/message-input/message-input.component.html +++ b/projects/stream-chat-angular/src/lib/message-input/message-input.component.html @@ -54,6 +54,10 @@ data-testid="file-input" [multiple]="isMultipleFileUploadEnabled" id="{{ fileInputId }}" + [disabled]=" + (attachmentService.attachmentsCounter$ | async)! >= + attachmentService.maxNumberOfAttachments + " (change)="filesSelected(fileInput.files)" />