diff --git a/projects/v3/src/app/components/topic/topic.component.html b/projects/v3/src/app/components/topic/topic.component.html index c5c4497c0..a8c1e643c 100644 --- a/projects/v3/src/app/components/topic/topic.component.html +++ b/projects/v3/src/app/components/topic/topic.component.html @@ -52,7 +52,7 @@ [title]="file.name" leadingIcon="document" lines="full" - [endingActionBtnIcons]="['download', 'search']" + [endingActionBtnIcons]="getFileActionIcons(file)" (actionBtnClick)="actionBtnClick(file, $event)" > diff --git a/projects/v3/src/app/components/topic/topic.component.spec.ts b/projects/v3/src/app/components/topic/topic.component.spec.ts index b4bcfa9ec..04cbc66fd 100644 --- a/projects/v3/src/app/components/topic/topic.component.spec.ts +++ b/projects/v3/src/app/components/topic/topic.component.spec.ts @@ -155,16 +155,32 @@ describe('TopicComponent', () => { expect(utilsSpy.downloadFile).toHaveBeenCalled(); }); - it('should call previewFile when index 1 and url is filestack', () => { + it('should call previewFile when index 1 and url is filestack with supported type', () => { spyOn(component, 'previewFile'); const file = { url: 'https://cdn.filestackcontent.com/abc123', name: 'doc.pdf' }; component.actionBtnClick(file, 1); expect(component.previewFile).toHaveBeenCalledWith(file); }); + it('should open video modal when index 1 and file is video', () => { + spyOn(component, 'previewVideoFile'); + const file = { url: 'https://cdn.filestackcontent.com/abc123.mp4', name: 'video.mp4' }; + component.actionBtnClick(file, 1); + expect(component.previewVideoFile).toHaveBeenCalledWith(file); + }); + + it('should open new tab when index 1 and url is filestack but file is audio', () => { + spyOn(window, 'open'); + spyOn(component, 'previewFile'); + const file = { url: 'https://cdn.filestackcontent.com/abc123', name: 'recording.mp3' }; + component.actionBtnClick(file, 1); + expect(component.previewFile).not.toHaveBeenCalled(); + expect(window.open).toHaveBeenCalledWith(file.url, '_blank'); + }); + it('should open new tab when index 1 and url is not filestack', () => { spyOn(window, 'open'); - const file = { url: 'https://example.com/video.mp4', name: 'video.mp4' }; + const file = { url: 'https://example.com/document.pdf', name: 'document.pdf' }; component.actionBtnClick(file, 1); expect(window.open).toHaveBeenCalledWith(file.url, '_blank'); expect(notificationSpy.presentToast).toHaveBeenCalled(); @@ -177,4 +193,64 @@ describe('TopicComponent', () => { expect(window.open).toHaveBeenCalledWith(file.url, '_blank'); }); }); + + describe('getFileActionIcons', () => { + it('should return both download and search icons for filestack url with supported type', () => { + const file = { url: 'https://cdn.filestackcontent.com/abc123', name: 'document.pdf' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download', 'search']); + }); + + it('should return both download and search icons for video files', () => { + const file = { url: 'https://cdn.filestackcontent.com/abc123.mp4', name: 'video.mp4' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download', 'search']); + }); + + it('should return both download and search icons for non-filestack video', () => { + const file = { url: 'https://example.com/video.mp4', name: 'video.mp4' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download', 'search']); + }); + + it('should return only download icon for filestack url with audio', () => { + const file = { url: 'https://cdn.filestackcontent.com/abc123', name: 'audio.mp3' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download']); + }); + + it('should return only download icon for non-filestack non-video file', () => { + const file = { url: 'https://example.com/file.pdf', name: 'document.pdf' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download']); + }); + + it('should return only download icon for non-mp4 video files', () => { + const file = { url: 'https://example.com/video.mov', name: 'video.mov' }; + const icons = component.getFileActionIcons(file); + expect(icons).toEqual(['download']); + }); + }); + + describe('previewVideoFile', () => { + it('should open video modal with file properties', async () => { + const modalSpy = jasmine.createSpyObj('Modal', ['present']); + spyOn(component['modalController'], 'create').and.returnValue(Promise.resolve(modalSpy)); + + const file = { url: 'https://example.com/video.mp4', name: 'test.mp4' }; + await component.previewVideoFile(file); + + expect(component['modalController'].create).toHaveBeenCalledWith({ + component: jasmine.anything(), + componentProps: { + file: { + url: file.url, + name: file.name, + type: 'video/mp4', + }, + }, + }); + expect(modalSpy.present).toHaveBeenCalled(); + }); + }); }); diff --git a/projects/v3/src/app/components/topic/topic.component.ts b/projects/v3/src/app/components/topic/topic.component.ts index 343b10ade..d03672d0e 100644 --- a/projects/v3/src/app/components/topic/topic.component.ts +++ b/projects/v3/src/app/components/topic/topic.component.ts @@ -11,6 +11,8 @@ import { NotificationsService } from '@v3/app/services/notifications.service'; import { BehaviorSubject, exhaustMap, filter, finalize, Subject, Subscription, takeUntil } from 'rxjs'; import { Task } from '@v3/app/services/activity.service'; import { ComponentCleanupService } from '@v3/app/services/component-cleanup.service'; +import { ModalController } from '@ionic/angular'; +import { FilePopupComponent } from '../file-popup/file-popup.component'; @Component({ selector: 'app-topic', @@ -52,6 +54,7 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe private sanitizer: DomSanitizer, private cleanupService: ComponentCleanupService, private cdr: ChangeDetectorRef, + private modalController: ModalController, @Inject(DOCUMENT) private readonly document: Document ) { this.isMobile = this.utils.isMobile(); @@ -304,11 +307,14 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe this.utils.downloadFile(file.url); break; case 1: - if (this._isFilestackUrl(file.url)) { + if (this._isVideoFile(file)) { + // show mp4 file in modal with html5 player + this.previewVideoFile(file); + } else if (this._isFilestackUrl(file.url) && this._isFilestackPreviewSupported(file)) { + // show filestack document viewer this.previewFile(file); } else { // non-filestack files: open in new tab as download fallback - this.notification.presentToast('Preview not available. Opening file in a new tab.'); window.open(file.url, '_blank'); } break; @@ -322,6 +328,56 @@ export class TopicComponent implements OnInit, OnChanges, AfterViewChecked, OnDe return url?.includes('filestackcontent') || false; } + /** + * @description checks if file is an mp4 video (html5 browser-supported format) + */ + private _isVideoFile(file: { url: string; name: string }): boolean { + const urlLower = (file.url || '').toLowerCase(); + const nameLower = (file.name || '').toLowerCase(); + return urlLower.endsWith('.mp4') || nameLower.endsWith('.mp4'); + } + + /** + * @description checks if a file type is supported by filestack document viewer. + * supported: pdf, ppt/pptx, xls/xlsx, doc/docx, odt, odp, images, html, txt, ai, psd. + * unsupported: audio files (videos handled separately by html5 player). + */ + private _isFilestackPreviewSupported(file: { url: string; name: string }): boolean { + const unsupportedExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.flac', '.wma', '.m4a']; + const urlLower = (file.url || '').toLowerCase(); + const nameLower = (file.name || '').toLowerCase(); + return !unsupportedExtensions.some(ext => urlLower.endsWith(ext) || nameLower.endsWith(ext)); + } + + /** + * @description preview mp4 file in modal with html5 video player + */ + async previewVideoFile(file: { url: string; name: string }): Promise { + const modal = await this.modalController.create({ + component: FilePopupComponent, + componentProps: { + file: { + url: file.url, + name: file.name, + type: 'video/mp4', + }, + }, + }); + return await modal.present(); + } + + /** + * @description returns action button icons for file attachment based on preview support. + * preview icon shown for: + * - mp4 video files (shown in html5 video modal) + * - filestack urls with document viewer supported file types + */ + getFileActionIcons(file: { url: string; name: string }): string[] { + const canPreview = this._isVideoFile(file) || + (this._isFilestackUrl(file.url) && this._isFilestackPreviewSupported(file)); + return canPreview ? ['download', 'search'] : ['download']; + } + async actionBarContinue(topic): Promise { if (this.continueAction$) { this.continueAction$.next(topic);