Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/v3/src/app/components/topic/topic.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
[title]="file.name"
leadingIcon="document"
lines="full"
[endingActionBtnIcons]="['download', 'search']"
[endingActionBtnIcons]="getFileActionIcons(file)"
(actionBtnClick)="actionBtnClick(file, $event)"
></app-list-item>

Expand Down
80 changes: 78 additions & 2 deletions projects/v3/src/app/components/topic/topic.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});
});
});
60 changes: 58 additions & 2 deletions projects/v3/src/app/components/topic/topic.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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<void> {
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<void> {
if (this.continueAction$) {
this.continueAction$.next(topic);
Expand Down