Skip to content

Commit 77b0c78

Browse files
authored
Merge pull request #257 from GetStream/video-attachment-support
feat: Support for video attachments #231
2 parents 7b9f6fc + 25b27c2 commit 77b0c78

File tree

9 files changed

+96
-11
lines changed

9 files changed

+96
-11
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
@@ -24,6 +24,7 @@ describe('AttachmentListComponent', () => {
2424
let queryImageModalPrevButton: () => HTMLButtonElement | null;
2525
let queryImageModalNextButton: () => HTMLButtonElement | null;
2626
let queryGallery: () => HTMLElement | null;
27+
let queryVideos: () => HTMLVideoElement[];
2728
let sendAction: jasmine.Spy;
2829

2930
const waitForImgComplete = () => {
@@ -92,6 +93,10 @@ describe('AttachmentListComponent', () => {
9293
) as HTMLButtonElement;
9394
queryGallery = () =>
9495
nativeElement.querySelector('[data-testid="image-gallery"]');
96+
queryVideos = () =>
97+
Array.from(
98+
nativeElement.querySelectorAll('[data-testclass="video-attachment"]')
99+
);
95100
});
96101

97102
it('should display received #attachments ordered', () => {
@@ -126,26 +131,26 @@ describe('AttachmentListComponent', () => {
126131
title_link: 'https://giphy.com/gifs/game-point-Eq5pb4dR4DJQc',
127132
type: 'giphy',
128133
},
134+
{
135+
type: 'video',
136+
asset_url: 'url6',
137+
},
129138
];
130139
component.ngOnChanges();
131140
fixture.detectChanges();
132141
const attachments = queryAttachments();
133142

134-
expect(attachments.length).toBe(5);
143+
expect(attachments.length).toBe(6);
135144
expect(
136145
attachments[0].classList.contains('str-chat__message-attachment--image')
137146
).toBeTrue();
138147

139148
expect(
140-
attachments[1].classList.contains('str-chat__message-attachment--file')
149+
attachments[1].classList.contains('str-chat__message-attachment--video')
141150
).toBeTrue();
142151

143152
expect(
144-
attachments[1].classList.contains('str-chat__message-attachment--image')
145-
).toBeFalse();
146-
147-
expect(
148-
attachments[2].classList.contains('str-chat__message-attachment--card')
153+
attachments[2].classList.contains('str-chat__message-attachment--file')
149154
).toBeTrue();
150155

151156
expect(
@@ -156,19 +161,28 @@ describe('AttachmentListComponent', () => {
156161
attachments[3].classList.contains('str-chat__message-attachment--card')
157162
).toBeTrue();
158163

164+
expect(
165+
attachments[3].classList.contains('str-chat__message-attachment--image')
166+
).toBeFalse();
167+
159168
expect(
160169
attachments[4].classList.contains('str-chat__message-attachment--card')
161170
).toBeTrue();
162171

163172
expect(
164-
attachments[4].classList.contains('str-chat__message-attachment--giphy')
173+
attachments[5].classList.contains('str-chat__message-attachment--card')
174+
).toBeTrue();
175+
176+
expect(
177+
attachments[5].classList.contains('str-chat__message-attachment--giphy')
165178
).toBeTrue();
166179

167180
expect(queryImages().length).toBe(1);
168181
expect(queryFileLinks().length).toBe(1);
169182
expect(queryUrlLinks().length).toBe(3);
170183
expect(queryCardImages().length).toBe(3);
171184
expect(queryActions().length).toBe(0);
185+
expect(queryVideos().length).toBe(1);
172186
});
173187

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

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
@@ -47,6 +47,7 @@ export class AttachmentListComponent implements OnChanges {
4747
const containsGallery = images.length >= 2;
4848
this.orderedAttachments = [
4949
...(containsGallery ? this.createGallery(images) : images),
50+
...this.attachments.filter((a) => this.isVideo(a)),
5051
...this.attachments.filter((a) => this.isFile(a)),
5152
...this.attachments.filter((a) => this.isCard(a)),
5253
];
@@ -68,6 +69,14 @@ export class AttachmentListComponent implements OnChanges {
6869
return attachment.type === 'gallery';
6970
}
7071

72+
isVideo(attachment: Attachment) {
73+
return (
74+
attachment.type === 'video' &&
75+
attachment.asset_url &&
76+
!attachment.og_scrape_url // links from video share services (such as YouTube or Facebook) are can't be played
77+
);
78+
}
79+
7180
isCard(attachment: Attachment) {
7281
return (
7382
!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
@@ -309,4 +309,16 @@ describe('AttachmentPreviewListComponent', () => {
309309

310310
expect(event.preventDefault).toHaveBeenCalledWith();
311311
});
312+
313+
it('should display video files as file attachments', () => {
314+
const upload = {
315+
file: { name: 'cute-video.mp4', type: 'video/mp4' } as File,
316+
state: 'success',
317+
type: 'video',
318+
} as AttachmentUpload;
319+
attachmentUploads$.next([upload]);
320+
fixture.detectChanges();
321+
322+
expect(queryPreviewFiles().length).toBe(1);
323+
});
312324
});

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

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

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

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/types.ts

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

0 commit comments

Comments
 (0)