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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AttachmentsScreenshot from "../assets/attachments-screenshot.png";
The `AttachmentList` compontent displays the attachments of a message. The following attachments are supported:

- Images (including GIFs) are displayed inline
- Videos are displayed inline
- Other files can be downloaded
- Links in a message are enriched with built-in open graph URL scraping

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@
</button>
</ng-container>
</div>
<video
*ngIf="isVideo(attachment)"
controls
data-testclass="video-attachment"
[src]="attachment.asset_url"
style="
width: 100%;
max-width: 400px;
height: 300px;
border-radius: inherit;
"
></video>
<div
*ngIf="isFile(attachment)"
class="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('AttachmentListComponent', () => {
let queryImageModalPrevButton: () => HTMLButtonElement | null;
let queryImageModalNextButton: () => HTMLButtonElement | null;
let queryGallery: () => HTMLElement | null;
let queryVideos: () => HTMLVideoElement[];
let sendAction: jasmine.Spy;

const waitForImgComplete = () => {
Expand Down Expand Up @@ -90,6 +91,10 @@ describe('AttachmentListComponent', () => {
) as HTMLButtonElement;
queryGallery = () =>
nativeElement.querySelector('[data-testid="image-gallery"]');
queryVideos = () =>
Array.from(
nativeElement.querySelectorAll('[data-testclass="video-attachment"]')
);
});

it('should display received #attachments ordered', () => {
Expand Down Expand Up @@ -124,26 +129,26 @@ describe('AttachmentListComponent', () => {
title_link: 'https://giphy.com/gifs/game-point-Eq5pb4dR4DJQc',
type: 'giphy',
},
{
type: 'video',
asset_url: 'url6',
},
];
component.ngOnChanges();
fixture.detectChanges();
const attachments = queryAttachments();

expect(attachments.length).toBe(5);
expect(attachments.length).toBe(6);
expect(
attachments[0].classList.contains('str-chat__message-attachment--image')
).toBeTrue();

expect(
attachments[1].classList.contains('str-chat__message-attachment--file')
attachments[1].classList.contains('str-chat__message-attachment--video')
).toBeTrue();

expect(
attachments[1].classList.contains('str-chat__message-attachment--image')
).toBeFalse();

expect(
attachments[2].classList.contains('str-chat__message-attachment--card')
attachments[2].classList.contains('str-chat__message-attachment--file')
).toBeTrue();

expect(
Expand All @@ -154,19 +159,28 @@ describe('AttachmentListComponent', () => {
attachments[3].classList.contains('str-chat__message-attachment--card')
).toBeTrue();

expect(
attachments[3].classList.contains('str-chat__message-attachment--image')
).toBeFalse();

expect(
attachments[4].classList.contains('str-chat__message-attachment--card')
).toBeTrue();

expect(
attachments[4].classList.contains('str-chat__message-attachment--giphy')
attachments[5].classList.contains('str-chat__message-attachment--card')
).toBeTrue();

expect(
attachments[5].classList.contains('str-chat__message-attachment--giphy')
).toBeTrue();

expect(queryImages().length).toBe(1);
expect(queryFileLinks().length).toBe(1);
expect(queryUrlLinks().length).toBe(3);
expect(queryCardImages().length).toBe(3);
expect(queryActions().length).toBe(0);
expect(queryVideos().length).toBe(1);
});

it('should create gallery', () => {
Expand Down Expand Up @@ -761,4 +775,23 @@ describe('AttachmentListComponent', () => {
expect(component.imagesToView).toEqual([]);
});
});

it(`shouldn't display video links as video attachments`, () => {
const attachment = {
asset_url: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
author_name: 'YouTube',
image_url: 'https://i.ytimg.com/vi/m4-HM_sCvtQ/mqdefault.jpg',
og_scrape_url: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
text: "Java is one of the most successful and most dreaded technologies in the computer science world. Let's roast this powerful open-source programming language to find out why it has so many haters. \n\n#java #programming #comedy #100SecondsOfCode\n\n🔗 Resources\n\nJava Website https://java.com\nJava in 100 Seconds https://youtu.be/l9AzO1FMgM8\nWhy Java Sucks https://tech.jonathangardner.net/wiki/Why_Java_Sucks\nWhy Java Doesn't Suck https://smartbear.com/blog/please-stop-staying-java-sucks/\n\n🔥 Get More Content - Upgrade to PRO\n\nUpgrade to Fireship PRO at https://fireship.io/pro\nUse code lORhwXd2 for ...",
thumb_url: 'https://i.ytimg.com/vi/m4-HM_sCvtQ/mqdefault.jpg',
title: 'Java for the Haters in 100 Seconds',
title_link: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
type: 'video',
};
component.attachments = [attachment];
component.ngOnChanges();
fixture.detectChanges();

expect(queryVideos().length).toBe(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class AttachmentListComponent implements OnChanges {
const containsGallery = images.length >= 2;
this.orderedAttachments = [
...(containsGallery ? this.createGallery(images) : images),
...this.attachments.filter((a) => this.isVideo(a)),
...this.attachments.filter((a) => this.isFile(a)),
...this.attachments.filter((a) => this.isCard(a)),
];
Expand All @@ -58,6 +59,14 @@ export class AttachmentListComponent implements OnChanges {
return attachment.type === 'gallery';
}

isVideo(attachment: Attachment) {
return (
attachment.type === 'video' &&
attachment.asset_url &&
!attachment.og_scrape_url // links from video share services (such as YouTube or Facebook) are can't be played
);
}

isCard(attachment: Attachment) {
return (
!attachment.type ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
</div>
<div
class="rfu-file-previewer"
*ngIf="attachmentUpload.type === 'file'"
*ngIf="
attachmentUpload.type === 'file' || attachmentUpload.type === 'video'
"
data-testclass="attachment-file-preview"
>
<ol>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,16 @@ describe('AttachmentPreviewListComponent', () => {

expect(event.preventDefault).toHaveBeenCalledWith();
});

it('should display video files as file attachments', () => {
const upload = {
file: { name: 'cute-video.mp4', type: 'video/mp4' } as File,
state: 'success',
type: 'video',
} as AttachmentUpload;
attachmentService.attachmentUploads$.next([upload]);
fixture.detectChanges();

expect(queryPreviewFiles().length).toBe(1);
});
});
10 changes: 9 additions & 1 deletion projects/stream-chat-angular/src/lib/attachment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,26 @@ describe('AttachmentService', () => {
{ type: 'image/vnd.adobe.photoshop' },
{ type: 'plain/text' },
];
const files = [...imageFiles, ...dataFiles];
const videoFiles = [
{ type: 'video/quicktime' },
{ type: 'video/x-msvideo' },
];
const files = [...imageFiles, ...dataFiles, ...videoFiles];
uploadAttachmentsSpy.and.resolveTo([
{ file: imageFiles[0], state: 'success', url: 'url1', type: 'image' },
{ file: imageFiles[1], state: 'success', url: 'url2', type: 'image' },
{ file: dataFiles[0], state: 'success', url: 'url3', type: 'file' },
{ file: dataFiles[1], state: 'success', url: 'url4', type: 'file' },
{ file: videoFiles[0], state: 'success', url: 'url5', type: 'video' },
{ file: videoFiles[1], state: 'success', url: 'url6', type: 'video' },
]);
void service.filesSelected(files as any as FileList);

expect(uploadAttachmentsSpy).toHaveBeenCalledWith([
{ file: imageFiles[0], type: 'image', state: 'uploading' },
{ file: imageFiles[1], type: 'image', state: 'uploading' },
{ file: videoFiles[0], type: 'video', state: 'uploading' },
{ file: videoFiles[1], type: 'video', state: 'uploading' },
{ file: dataFiles[0], type: 'file', state: 'uploading' },
{ file: dataFiles[1], type: 'file', state: 'uploading' },
]);
Expand Down
8 changes: 8 additions & 0 deletions projects/stream-chat-angular/src/lib/attachment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ export class AttachmentService {
}
const imageFiles: File[] = [];
const dataFiles: File[] = [];
const videoFiles: File[] = [];

Array.from(fileList).forEach((file) => {
if (isImageFile(file)) {
imageFiles.push(file);
} else if (file.type.startsWith('video/')) {
videoFiles.push(file);
} else {
dataFiles.push(file);
}
Expand All @@ -70,6 +73,11 @@ export class AttachmentService {
state: 'uploading' as 'uploading',
type: 'image' as 'image',
})),
...videoFiles.map((file) => ({
file,
state: 'uploading' as 'uploading',
type: 'video' as 'video',
})),
...dataFiles.map((file) => ({
file,
state: 'uploading' as 'uploading',
Expand Down
102 changes: 102 additions & 0 deletions projects/stream-chat-angular/src/lib/channel.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,4 +1358,106 @@ describe('ChannelService', () => {

expect(spy).toHaveBeenCalledWith({ channel1: newMessage.created_at });
});

it('should call custom #customFileUploadRequest and #customImageUploadRequest if provided', async () => {
await init();
let channel!: Channel;
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
const customImageUploadRequest = jasmine
.createSpy()
.and.callFake((file: File) => {
switch (file.name) {
case 'file_error.jpg':
return Promise.reject(new Error());
default:
return Promise.resolve({ file: 'url/to/image' });
}
});
const customFileUploadRequest = jasmine
.createSpy()
.and.callFake((file: File) => {
switch (file.name) {
case 'file_error.pdf':
return Promise.reject(new Error());
default:
return Promise.resolve({ file: 'url/to/pdf' });
}
});
service.customImageUploadRequest = customImageUploadRequest;
service.customFileUploadRequest = customFileUploadRequest;
spyOn(channel, 'sendImage');
spyOn(channel, 'sendFile');
const file1 = { name: 'food.png' } as File;
const file2 = { name: 'file_error.jpg' } as File;
const file3 = { name: 'menu.pdf' } as File;
const file4 = { name: 'file_error.pdf' } as File;
const attachments = [
{ file: file1, type: 'image', state: 'uploading' },
{ file: file2, type: 'image', state: 'uploading' },
{ file: file3, type: 'file', state: 'uploading' },
{ file: file4, type: 'file', state: 'uploading' },
] as AttachmentUpload[];
const result = await service.uploadAttachments(attachments);
const expectedResult = [
{
file: file1,
state: 'success',
url: 'url/to/image',
type: 'image',
},
{ file: file2, state: 'error', type: 'image' },
{
file: file3,
state: 'success',
url: 'url/to/pdf',
type: 'file',
},
{ file: file4, state: 'error', type: 'file' },
];

expect(channel.sendImage).not.toHaveBeenCalled();
expect(channel.sendFile).not.toHaveBeenCalled();

expectedResult.forEach((r, i) => {
expect(r).toEqual(result[i]);
});
});

it('should call custom #customImageDeleteRequest if provided', async () => {
await init();
let channel!: Channel;
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
const customImageDeleteRequest = jasmine.createSpy();
service.customImageDeleteRequest = customImageDeleteRequest;
spyOn(channel, 'deleteImage');
const url = 'url/to/image';
await service.deleteAttachment({
url,
type: 'image',
state: 'success',
file: {} as any as File,
});

expect(customImageDeleteRequest).toHaveBeenCalledWith(url, channel);
expect(channel.deleteImage).not.toHaveBeenCalled();
});

it('should call custom #customFileDeleteRequest if provided', async () => {
await init();
let channel!: Channel;
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
const customFileDeleteRequest = jasmine.createSpy();
service.customFileDeleteRequest = customFileDeleteRequest;
spyOn(channel, 'deleteFile');
const url = 'url/to/file';
await service.deleteAttachment({
url,
type: 'file',
state: 'success',
file: {} as any as File,
});

expect(customFileDeleteRequest).toHaveBeenCalledWith(url, channel);
expect(channel.deleteFile).not.toHaveBeenCalled();
});
});
Loading