Skip to content

Commit 3df854a

Browse files
committed
feat: Support for video attachments #231
1 parent 7ed2558 commit 3df854a

File tree

10 files changed

+99
-14
lines changed

10 files changed

+99
-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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,7 +1361,7 @@ describe('ChannelService', () => {
13611361

13621362
it('should call custom #customFileUploadRequest and #customImageUploadRequest if provided', async () => {
13631363
await init();
1364-
let channel!: Channel<DefaultStreamChatGenerics>;
1364+
let channel!: Channel;
13651365
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
13661366
const customImageUploadRequest = jasmine
13671367
.createSpy()
@@ -1425,7 +1425,7 @@ describe('ChannelService', () => {
14251425

14261426
it('should call custom #customImageDeleteRequest if provided', async () => {
14271427
await init();
1428-
let channel!: Channel<DefaultStreamChatGenerics>;
1428+
let channel!: Channel;
14291429
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
14301430
const customImageDeleteRequest = jasmine.createSpy();
14311431
service.customImageDeleteRequest = customImageDeleteRequest;
@@ -1444,7 +1444,7 @@ describe('ChannelService', () => {
14441444

14451445
it('should call custom #customFileDeleteRequest if provided', async () => {
14461446
await init();
1447-
let channel!: Channel<DefaultStreamChatGenerics>;
1447+
let channel!: Channel;
14481448
service.activeChannel$.pipe(first()).subscribe((c) => (channel = c!));
14491449
const customFileDeleteRequest = jasmine.createSpy();
14501450
service.customFileDeleteRequest = customFileDeleteRequest;

projects/stream-chat-angular/src/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export type AttachmentUpload = {
8080
file: File;
8181
state: 'error' | 'success' | 'uploading';
8282
url?: string;
83-
type: 'image' | 'file';
83+
type: 'image' | 'file' | 'video';
8484
previewUri?: string | ArrayBuffer;
8585
};
8686

0 commit comments

Comments
 (0)