0" class="str-chat__attachment-list">
+
0 ||
+ (customAttachments.length > 0 && customAttachmentsTemplate)
+ "
+ class="str-chat__attachment-list"
+>
@@ -414,6 +420,18 @@
+
+
+
0">
{
let component: AttachmentListComponent;
@@ -50,6 +61,8 @@ describe('AttachmentListComponent', () => {
{ provide: ChannelService, useValue: { sendAction: sendAction } },
StreamI18nService,
AttachmentConfigurationService,
+ CustomTemplatesService,
+ MessageService,
],
imports: [TranslateModule.forRoot()],
}).compileComponents();
@@ -174,6 +187,24 @@ describe('AttachmentListComponent', () => {
expect(queryVideos().length).toBe(1);
});
+ it('should filter custom attachments', () => {
+ const messageService = TestBed.inject(MessageService);
+ messageService.filterCustomAttachment = (attachment: Attachment) =>
+ !attachment.customLink;
+ const imageAttachment = {
+ type: 'image',
+ image_url: 'url/to/image',
+ };
+ const customImageAttachment = {
+ type: 'image',
+ customLink: 'load/from/here',
+ };
+ component.attachments = [imageAttachment, customImageAttachment];
+ component.ngOnChanges({ attachments: {} as SimpleChange });
+
+ expect(component.orderedAttachments.length).toBe(1);
+ });
+
it('should display voice recording', () => {
component.attachments = [mockVoiceRecording];
component.ngOnChanges({ attachments: {} as SimpleChange });
@@ -558,6 +589,7 @@ describe('AttachmentListComponent', () => {
const thumbUrl = 'https://getstream.io/images/og/OG_Home.png';
component.attachments = [
{
+ type: 'image',
author_name: 'GetStream',
image_url: undefined,
og_scrape_url: 'https://getstream.io',
@@ -603,6 +635,7 @@ describe('AttachmentListComponent', () => {
thumb_url: 'https://getstream.io/images/og/OG_Home.png',
title,
title_link: '/',
+ type: 'image',
},
];
component.ngOnChanges({ attachments: {} as SimpleChange });
@@ -626,6 +659,7 @@ describe('AttachmentListComponent', () => {
title: 'Stream',
text,
title_link: '/',
+ type: 'image',
},
];
component.ngOnChanges({ attachments: {} as SimpleChange });
@@ -641,6 +675,7 @@ describe('AttachmentListComponent', () => {
const titleLink = 'https://getstream.io';
component.attachments = [
{
+ type: 'image',
author_name: 'GetStream',
image_url: undefined,
og_scrape_url: 'https://getstream.io/home',
@@ -668,6 +703,7 @@ describe('AttachmentListComponent', () => {
title: 'Stream',
text: 'Build scalable in-app chat or activity feeds in days. Product teams trust Stream to launch faster, iterate more often, and ship a better user experience.',
title_link: undefined,
+ type: 'image',
},
];
component.ngOnChanges({ attachments: {} as SimpleChange });
@@ -1000,3 +1036,101 @@ describe('AttachmentListComponent', () => {
expect(videoElements[0].poster).toContain(attachments[0].thumb_url);
});
});
+
+describe('AttachmentListComponent with custom attachments', () => {
+ @Component({
+ selector: 'stream-test-component-attachment-list',
+ template: `
+
+
+
+
+ Use the following
+
payment lint to
+ pay me {{ value }}.
+
+
+
+ `,
+ })
+ class TestHostComponentAttachmentListComponent implements AfterViewInit {
+ @ViewChild('customAttachments')
+ template!: TemplateRef;
+ attachments: Attachment[] = [];
+ constructor(private customTemplatesService: CustomTemplatesService) {}
+
+ ngAfterViewInit(): void {
+ this.customTemplatesService.customAttachmentListTemplate$.next(
+ this.template
+ );
+ }
+ }
+
+ let hostComponent: TestHostComponentAttachmentListComponent;
+ let hostFixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ AttachmentListComponent,
+ TestHostComponentAttachmentListComponent,
+ ],
+ providers: [CustomTemplatesService],
+ }).compileComponents();
+
+ hostFixture = TestBed.createComponent(
+ TestHostComponentAttachmentListComponent
+ );
+ hostComponent = hostFixture.componentInstance;
+ hostFixture.detectChanges();
+ });
+
+ it('should display custom attachments', () => {
+ expect(
+ hostFixture.nativeElement.querySelectorAll('.payment-link').length
+ ).toBe(0);
+
+ const customAttachment = {
+ type: 'custom',
+ subtype: 'payment',
+ value: '30$',
+ link: 'pay/me/or/else',
+ };
+ hostComponent.attachments = [customAttachment];
+ hostFixture.detectChanges();
+
+ expect(
+ hostFixture.nativeElement.querySelectorAll('.payment-link').length
+ ).toBe(1);
+ });
+
+ it(`shouldn't display attachments if no template is provided`, () => {
+ const customTemplatesService = TestBed.inject(CustomTemplatesService);
+ customTemplatesService.customAttachmentListTemplate$.next(undefined);
+
+ const customAttachment = {
+ type: 'custom',
+ subtype: 'payment',
+ value: '30$',
+ link: 'pay/me/or/else',
+ };
+ hostComponent.attachments = [customAttachment];
+ hostFixture.detectChanges();
+
+ expect(
+ hostFixture.nativeElement.querySelector('.str-chat__attachment-list')
+ ).toBeNull();
+ });
+
+ it(`shouldn't display attachments if there are no attachments`, () => {
+ expect(
+ hostFixture.nativeElement.querySelector('.str-chat__attachment-list')
+ ).toBeNull();
+ });
+});
diff --git a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts
index c83410a5..3ce94f68 100644
--- a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts
+++ b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts
@@ -4,6 +4,8 @@ import {
HostBinding,
Input,
OnChanges,
+ OnDestroy,
+ OnInit,
Output,
SimpleChanges,
TemplateRef,
@@ -17,12 +19,15 @@ import {
VideoAttachmentConfiguration,
ImageAttachmentConfiguration,
AttachmentContext,
+ CustomAttachmentListContext,
} from '../types';
import prettybytes from 'pretty-bytes';
import { isImageAttachment } from '../is-image-attachment';
import { ChannelService } from '../channel.service';
import { CustomTemplatesService } from '../custom-templates.service';
import { AttachmentConfigurationService } from '../attachment-configuration.service';
+import { Subscription } from 'rxjs';
+import { MessageService } from '../message.service';
/**
* The `AttachmentList` component displays the attachments of a message
@@ -32,7 +37,7 @@ import { AttachmentConfigurationService } from '../attachment-configuration.serv
templateUrl: './attachment-list.component.html',
styles: [],
})
-export class AttachmentListComponent implements OnChanges {
+export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy {
/**
* The id of the message the attachments belong to
*/
@@ -53,8 +58,10 @@ export class AttachmentListComponent implements OnChanges {
>();
@HostBinding() class = 'str-chat__attachment-list-angular-host';
orderedAttachments: Attachment[] = [];
+ customAttachments: Attachment[] = [];
imagesToView: Attachment[] = [];
imagesToViewCurrentIndex = 0;
+ customAttachmentsTemplate?: TemplateRef;
@ViewChild('modalContent', { static: true })
private modalContent!: TemplateRef;
private attachmentConfigurations: Map<
@@ -63,34 +70,58 @@ export class AttachmentListComponent implements OnChanges {
| VideoAttachmentConfiguration
| ImageAttachmentConfiguration
> = new Map();
+ private subscriptions: Subscription[] = [];
constructor(
public readonly customTemplatesService: CustomTemplatesService,
private channelService: ChannelService,
- private attachmentConfigurationService: AttachmentConfigurationService
+ private attachmentConfigurationService: AttachmentConfigurationService,
+ private messageService: MessageService
) {}
+ ngOnInit(): void {
+ this.subscriptions.push(
+ this.customTemplatesService.customAttachmentListTemplate$.subscribe(
+ (t) => (this.customAttachmentsTemplate = t)
+ )
+ );
+ }
+
ngOnChanges(changes: SimpleChanges): void {
if (changes.attachments) {
- const images = this.attachments.filter(this.isImage);
+ const builtInAttachments: Attachment[] = [];
+ const customAttachments: Attachment[] = [];
+ this.attachments.forEach((a) => {
+ if (this.messageService.isCustomAttachment(a)) {
+ customAttachments.push(a);
+ } else {
+ builtInAttachments.push(a);
+ }
+ });
+ const images = builtInAttachments.filter(this.isImage);
const containsGallery = images.length >= 2;
this.orderedAttachments = [
...(containsGallery ? this.createGallery(images) : images),
- ...this.attachments.filter((a) => this.isVideo(a)),
- ...this.attachments.filter((a) => this.isVoiceMessage(a)),
- ...this.attachments.filter((a) => this.isFile(a)),
+ ...builtInAttachments.filter((a) => this.isVideo(a)),
+ ...builtInAttachments.filter((a) => this.isVoiceMessage(a)),
+ ...builtInAttachments.filter((a) => this.isFile(a)),
];
this.attachmentConfigurations = new Map();
// Display link attachments only if there are no other attachments
// Giphy-s always sent without other attachments
if (this.orderedAttachments.length === 0) {
this.orderedAttachments.push(
- ...this.attachments.filter((a) => this.isCard(a))
+ ...builtInAttachments.filter((a) => this.isCard(a))
);
}
+ this.customAttachments = customAttachments;
}
}
+ ngOnDestroy(): void {
+ this.subscriptions.forEach((s) => s.unsubscribe());
+ }
+
trackByUrl(_: number, attachment: Attachment) {
return (
attachment.image_url ||
diff --git a/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.html b/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.html
index 5a0525d1..b72923ff 100644
--- a/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.html
+++ b/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.html
@@ -1,5 +1,8 @@
0 && customAttachmentsPreview)
+ "
class="str-chat__attachment-preview-list"
>
@@ -101,6 +104,14 @@
>
+
+
+
diff --git a/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.spec.ts b/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.spec.ts
index 32726c11..a52a47e7 100644
--- a/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.spec.ts
+++ b/projects/stream-chat-angular/src/lib/attachment-preview-list/attachment-preview-list.component.spec.ts
@@ -1,8 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject, Subject } from 'rxjs';
-import { AttachmentUpload } from '../types';
+import { AttachmentUpload, CustomAttachmentPreviewListContext } from '../types';
import { AttachmentPreviewListComponent } from './attachment-preview-list.component';
+import {
+ AfterViewInit,
+ Component,
+ TemplateRef,
+ ViewChild,
+} from '@angular/core';
+import { CustomTemplatesService } from '../custom-templates.service';
+import { AttachmentService } from '../attachment.service';
describe('AttachmentPreviewListComponent', () => {
let component: AttachmentPreviewListComponent;
@@ -299,3 +307,96 @@ describe('AttachmentPreviewListComponent', () => {
expect(queryPreviewFiles().length).toBe(1);
});
});
+
+describe('AttachmentPreviewListComponent with custom attachments', () => {
+ @Component({
+ selector: 'stream-test-component',
+ template: `