Skip to content

Commit 9b03204

Browse files
authored
Merge pull request #258 from GetStream/custom-cdn-and-video-attachment
Custom cdn and video attachment
2 parents 8db8285 + 3df854a commit 9b03204

File tree

11 files changed

+231
-14
lines changed

11 files changed

+231
-14
lines changed

docusaurus/docs/Angular/components/AttachmentListComponent.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AttachmentsScreenshot from "../assets/attachments-screenshot.png";
33
The `AttachmentList` compontent displays the attachments of a message. The following attachments are supported:
44

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

projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@
7474
</button>
7575
</ng-container>
7676
</div>
77+
<video
78+
*ngIf="isVideo(attachment)"
79+
controls
80+
data-testclass="video-attachment"
81+
[src]="attachment.asset_url"
82+
style="
83+
width: 100%;
84+
max-width: 400px;
85+
height: 300px;
86+
border-radius: inherit;
87+
"
88+
></video>
7789
<div
7890
*ngIf="isFile(attachment)"
7991
class="

projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.spec.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('AttachmentListComponent', () => {
2222
let queryImageModalPrevButton: () => HTMLButtonElement | null;
2323
let queryImageModalNextButton: () => HTMLButtonElement | null;
2424
let queryGallery: () => HTMLElement | null;
25+
let queryVideos: () => HTMLVideoElement[];
2526
let sendAction: jasmine.Spy;
2627

2728
const waitForImgComplete = () => {
@@ -90,6 +91,10 @@ describe('AttachmentListComponent', () => {
9091
) as HTMLButtonElement;
9192
queryGallery = () =>
9293
nativeElement.querySelector('[data-testid="image-gallery"]');
94+
queryVideos = () =>
95+
Array.from(
96+
nativeElement.querySelectorAll('[data-testclass="video-attachment"]')
97+
);
9398
});
9499

95100
it('should display received #attachments ordered', () => {
@@ -124,26 +129,26 @@ describe('AttachmentListComponent', () => {
124129
title_link: 'https://giphy.com/gifs/game-point-Eq5pb4dR4DJQc',
125130
type: 'giphy',
126131
},
132+
{
133+
type: 'video',
134+
asset_url: 'url6',
135+
},
127136
];
128137
component.ngOnChanges();
129138
fixture.detectChanges();
130139
const attachments = queryAttachments();
131140

132-
expect(attachments.length).toBe(5);
141+
expect(attachments.length).toBe(6);
133142
expect(
134143
attachments[0].classList.contains('str-chat__message-attachment--image')
135144
).toBeTrue();
136145

137146
expect(
138-
attachments[1].classList.contains('str-chat__message-attachment--file')
147+
attachments[1].classList.contains('str-chat__message-attachment--video')
139148
).toBeTrue();
140149

141150
expect(
142-
attachments[1].classList.contains('str-chat__message-attachment--image')
143-
).toBeFalse();
144-
145-
expect(
146-
attachments[2].classList.contains('str-chat__message-attachment--card')
151+
attachments[2].classList.contains('str-chat__message-attachment--file')
147152
).toBeTrue();
148153

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

162+
expect(
163+
attachments[3].classList.contains('str-chat__message-attachment--image')
164+
).toBeFalse();
165+
157166
expect(
158167
attachments[4].classList.contains('str-chat__message-attachment--card')
159168
).toBeTrue();
160169

161170
expect(
162-
attachments[4].classList.contains('str-chat__message-attachment--giphy')
171+
attachments[5].classList.contains('str-chat__message-attachment--card')
172+
).toBeTrue();
173+
174+
expect(
175+
attachments[5].classList.contains('str-chat__message-attachment--giphy')
163176
).toBeTrue();
164177

165178
expect(queryImages().length).toBe(1);
166179
expect(queryFileLinks().length).toBe(1);
167180
expect(queryUrlLinks().length).toBe(3);
168181
expect(queryCardImages().length).toBe(3);
169182
expect(queryActions().length).toBe(0);
183+
expect(queryVideos().length).toBe(1);
170184
});
171185

172186
it('should create gallery', () => {
@@ -761,4 +775,23 @@ describe('AttachmentListComponent', () => {
761775
expect(component.imagesToView).toEqual([]);
762776
});
763777
});
778+
779+
it(`shouldn't display video links as video attachments`, () => {
780+
const attachment = {
781+
asset_url: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
782+
author_name: 'YouTube',
783+
image_url: 'https://i.ytimg.com/vi/m4-HM_sCvtQ/mqdefault.jpg',
784+
og_scrape_url: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
785+
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 ...",
786+
thumb_url: 'https://i.ytimg.com/vi/m4-HM_sCvtQ/mqdefault.jpg',
787+
title: 'Java for the Haters in 100 Seconds',
788+
title_link: 'https://www.youtube.com/watch?v=m4-HM_sCvtQ',
789+
type: 'video',
790+
};
791+
component.attachments = [attachment];
792+
component.ngOnChanges();
793+
fixture.detectChanges();
794+
795+
expect(queryVideos().length).toBe(0);
796+
});
764797
});

projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class AttachmentListComponent implements OnChanges {
3737
const containsGallery = images.length >= 2;
3838
this.orderedAttachments = [
3939
...(containsGallery ? this.createGallery(images) : images),
40+
...this.attachments.filter((a) => this.isVideo(a)),
4041
...this.attachments.filter((a) => this.isFile(a)),
4142
...this.attachments.filter((a) => this.isCard(a)),
4243
];
@@ -58,6 +59,14 @@ export class AttachmentListComponent implements OnChanges {
5859
return attachment.type === 'gallery';
5960
}
6061

62+
isVideo(attachment: Attachment) {
63+
return (
64+
attachment.type === 'video' &&
65+
attachment.asset_url &&
66+
!attachment.og_scrape_url // links from video share services (such as YouTube or Facebook) are can't be played
67+
);
68+
}
69+
6170
isCard(attachment: Attachment) {
6271
return (
6372
!attachment.type ||

projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
</div>
5555
<div
5656
class="rfu-file-previewer"
57-
*ngIf="attachmentUpload.type === 'file'"
57+
*ngIf="
58+
attachmentUpload.type === 'file' || attachmentUpload.type === 'video'
59+
"
5860
data-testclass="attachment-file-preview"
5961
>
6062
<ol>

projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,16 @@ describe('AttachmentPreviewListComponent', () => {
329329

330330
expect(event.preventDefault).toHaveBeenCalledWith();
331331
});
332+
333+
it('should display video files as file attachments', () => {
334+
const upload = {
335+
file: { name: 'cute-video.mp4', type: 'video/mp4' } as File,
336+
state: 'success',
337+
type: 'video',
338+
} as AttachmentUpload;
339+
attachmentService.attachmentUploads$.next([upload]);
340+
fixture.detectChanges();
341+
342+
expect(queryPreviewFiles().length).toBe(1);
343+
});
332344
});

projects/stream-chat-angular/src/lib/attachment.service.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,26 @@ describe('AttachmentService', () => {
187187
{ type: 'image/vnd.adobe.photoshop' },
188188
{ type: 'plain/text' },
189189
];
190-
const files = [...imageFiles, ...dataFiles];
190+
const videoFiles = [
191+
{ type: 'video/quicktime' },
192+
{ type: 'video/x-msvideo' },
193+
];
194+
const files = [...imageFiles, ...dataFiles, ...videoFiles];
191195
uploadAttachmentsSpy.and.resolveTo([
192196
{ file: imageFiles[0], state: 'success', url: 'url1', type: 'image' },
193197
{ file: imageFiles[1], state: 'success', url: 'url2', type: 'image' },
194198
{ file: dataFiles[0], state: 'success', url: 'url3', type: 'file' },
195199
{ file: dataFiles[1], state: 'success', url: 'url4', type: 'file' },
200+
{ file: videoFiles[0], state: 'success', url: 'url5', type: 'video' },
201+
{ file: videoFiles[1], state: 'success', url: 'url6', type: 'video' },
196202
]);
197203
void service.filesSelected(files as any as FileList);
198204

199205
expect(uploadAttachmentsSpy).toHaveBeenCalledWith([
200206
{ file: imageFiles[0], type: 'image', state: 'uploading' },
201207
{ file: imageFiles[1], type: 'image', state: 'uploading' },
208+
{ file: videoFiles[0], type: 'video', state: 'uploading' },
209+
{ file: videoFiles[1], type: 'video', state: 'uploading' },
202210
{ file: dataFiles[0], type: 'file', state: 'uploading' },
203211
{ file: dataFiles[1], type: 'file', state: 'uploading' },
204212
]);

projects/stream-chat-angular/src/lib/attachment.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ export class AttachmentService {
5555
}
5656
const imageFiles: File[] = [];
5757
const dataFiles: File[] = [];
58+
const videoFiles: File[] = [];
5859

5960
Array.from(fileList).forEach((file) => {
6061
if (isImageFile(file)) {
6162
imageFiles.push(file);
63+
} else if (file.type.startsWith('video/')) {
64+
videoFiles.push(file);
6265
} else {
6366
dataFiles.push(file);
6467
}
@@ -70,6 +73,11 @@ export class AttachmentService {
7073
state: 'uploading' as 'uploading',
7174
type: 'image' as 'image',
7275
})),
76+
...videoFiles.map((file) => ({
77+
file,
78+
state: 'uploading' as 'uploading',
79+
type: 'video' as 'video',
80+
})),
7381
...dataFiles.map((file) => ({
7482
file,
7583
state: 'uploading' as 'uploading',

projects/stream-chat-angular/src/lib/channel.service.spec.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,4 +1358,106 @@ describe('ChannelService', () => {
13581358

13591359
expect(spy).toHaveBeenCalledWith({ channel1: newMessage.created_at });
13601360
});
1361+
1362+
it('should call custom #customFileUploadRequest and #customImageUploadRequest if provided', async () => {
1363+
await init();
1364+
let channel!: Channel;
1365+
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
1366+
const customImageUploadRequest = jasmine
1367+
.createSpy()
1368+
.and.callFake((file: File) => {
1369+
switch (file.name) {
1370+
case 'file_error.jpg':
1371+
return Promise.reject(new Error());
1372+
default:
1373+
return Promise.resolve({ file: 'url/to/image' });
1374+
}
1375+
});
1376+
const customFileUploadRequest = jasmine
1377+
.createSpy()
1378+
.and.callFake((file: File) => {
1379+
switch (file.name) {
1380+
case 'file_error.pdf':
1381+
return Promise.reject(new Error());
1382+
default:
1383+
return Promise.resolve({ file: 'url/to/pdf' });
1384+
}
1385+
});
1386+
service.customImageUploadRequest = customImageUploadRequest;
1387+
service.customFileUploadRequest = customFileUploadRequest;
1388+
spyOn(channel, 'sendImage');
1389+
spyOn(channel, 'sendFile');
1390+
const file1 = { name: 'food.png' } as File;
1391+
const file2 = { name: 'file_error.jpg' } as File;
1392+
const file3 = { name: 'menu.pdf' } as File;
1393+
const file4 = { name: 'file_error.pdf' } as File;
1394+
const attachments = [
1395+
{ file: file1, type: 'image', state: 'uploading' },
1396+
{ file: file2, type: 'image', state: 'uploading' },
1397+
{ file: file3, type: 'file', state: 'uploading' },
1398+
{ file: file4, type: 'file', state: 'uploading' },
1399+
] as AttachmentUpload[];
1400+
const result = await service.uploadAttachments(attachments);
1401+
const expectedResult = [
1402+
{
1403+
file: file1,
1404+
state: 'success',
1405+
url: 'url/to/image',
1406+
type: 'image',
1407+
},
1408+
{ file: file2, state: 'error', type: 'image' },
1409+
{
1410+
file: file3,
1411+
state: 'success',
1412+
url: 'url/to/pdf',
1413+
type: 'file',
1414+
},
1415+
{ file: file4, state: 'error', type: 'file' },
1416+
];
1417+
1418+
expect(channel.sendImage).not.toHaveBeenCalled();
1419+
expect(channel.sendFile).not.toHaveBeenCalled();
1420+
1421+
expectedResult.forEach((r, i) => {
1422+
expect(r).toEqual(result[i]);
1423+
});
1424+
});
1425+
1426+
it('should call custom #customImageDeleteRequest if provided', async () => {
1427+
await init();
1428+
let channel!: Channel;
1429+
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
1430+
const customImageDeleteRequest = jasmine.createSpy();
1431+
service.customImageDeleteRequest = customImageDeleteRequest;
1432+
spyOn(channel, 'deleteImage');
1433+
const url = 'url/to/image';
1434+
await service.deleteAttachment({
1435+
url,
1436+
type: 'image',
1437+
state: 'success',
1438+
file: {} as any as File,
1439+
});
1440+
1441+
expect(customImageDeleteRequest).toHaveBeenCalledWith(url, channel);
1442+
expect(channel.deleteImage).not.toHaveBeenCalled();
1443+
});
1444+
1445+
it('should call custom #customFileDeleteRequest if provided', async () => {
1446+
await init();
1447+
let channel!: Channel;
1448+
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
1449+
const customFileDeleteRequest = jasmine.createSpy();
1450+
service.customFileDeleteRequest = customFileDeleteRequest;
1451+
spyOn(channel, 'deleteFile');
1452+
const url = 'url/to/file';
1453+
await service.deleteAttachment({
1454+
url,
1455+
type: 'file',
1456+
state: 'success',
1457+
file: {} as any as File,
1458+
});
1459+
1460+
expect(customFileDeleteRequest).toHaveBeenCalledWith(url, channel);
1461+
expect(channel.deleteFile).not.toHaveBeenCalled();
1462+
});
13611463
});

0 commit comments

Comments
 (0)