From d84497ce40cfc4b1a6705bb28a96343baf5852bc Mon Sep 17 00:00:00 2001 From: ANDREI PADOLIN Date: Tue, 15 Jun 2021 22:29:12 +0300 Subject: [PATCH] feat(chat): add support for custom message template (#2750) BREAKING CHANGE: `NbChatMessageComponent` constructor now has a second parameter (`NbCustomMessageService`). --- docs/structure.ts | 1 + src/app/playground-components.ts | 6 + .../chat/_chat.component.theme.scss | 44 ++++++ .../components/chat/chat-avatar.component.ts | 23 +++ .../chat-custom-message.directive.spec.ts | 37 +++++ .../chat/chat-custom-message.directive.ts | 79 ++++++++++ .../chat/chat-message-quote.component.spec.ts | 63 ++++++++ .../chat/chat-message-text.component.spec.ts | 71 +++++++++ .../chat/chat-message.component.spec.ts | 142 ++++++++++++++++++ .../components/chat/chat-message.component.ts | 116 +++++++++++--- .../theme/components/chat/chat.component.ts | 73 +++++++++ .../theme/components/chat/chat.module.ts | 9 ++ .../components/chat/custom-message.service.ts | 25 +++ src/framework/theme/public_api.ts | 2 + .../chat/chat-custom-message.component.html | 32 ++++ .../chat/chat-custom-message.component.scss | 13 ++ .../chat/chat-custom-message.component.ts | 100 ++++++++++++ .../with-layout/chat/chat-routing.module.ts | 5 + .../with-layout/chat/chat.module.ts | 11 +- .../chat-custom-message-table.component.scss | 25 +++ .../chat-custom-message-table.component.ts | 33 ++++ 21 files changed, 887 insertions(+), 23 deletions(-) create mode 100644 src/framework/theme/components/chat/chat-avatar.component.ts create mode 100644 src/framework/theme/components/chat/chat-custom-message.directive.spec.ts create mode 100644 src/framework/theme/components/chat/chat-custom-message.directive.ts create mode 100644 src/framework/theme/components/chat/chat-message-quote.component.spec.ts create mode 100644 src/framework/theme/components/chat/chat-message-text.component.spec.ts create mode 100644 src/framework/theme/components/chat/chat-message.component.spec.ts create mode 100644 src/framework/theme/components/chat/custom-message.service.ts create mode 100644 src/playground/with-layout/chat/chat-custom-message.component.html create mode 100644 src/playground/with-layout/chat/chat-custom-message.component.scss create mode 100644 src/playground/with-layout/chat/chat-custom-message.component.ts create mode 100644 src/playground/with-layout/chat/components/chat-custom-message-table.component.scss create mode 100644 src/playground/with-layout/chat/components/chat-custom-message-table.component.ts diff --git a/docs/structure.ts b/docs/structure.ts index a07170af06..1a64b3aa3c 100644 --- a/docs/structure.ts +++ b/docs/structure.ts @@ -693,6 +693,7 @@ export const structure = [ 'NbChatComponent', 'NbChatMessageComponent', 'NbChatFormComponent', + 'NbChatCustomMessageDirective', ], }, { diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts index 60bb1f4a39..0535a359a1 100644 --- a/src/app/playground-components.ts +++ b/src/app/playground-components.ts @@ -474,6 +474,12 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [ component: 'ChatTestComponent', name: 'Chat Test', }, + { + path: 'chat-custom-message.component', + link: '/chat/chat-custom-message.component', + component: 'ChatCustomMessageComponent', + name: 'Chat Custom Message', + }, ], }, { diff --git a/src/framework/theme/components/chat/_chat.component.theme.scss b/src/framework/theme/components/chat/_chat.component.theme.scss index b2f51426fa..1c892228b4 100644 --- a/src/framework/theme/components/chat/_chat.component.theme.scss +++ b/src/framework/theme/components/chat/_chat.component.theme.scss @@ -82,6 +82,7 @@ } .avatar { + display: block; border-radius: 50%; flex-shrink: 0; background: nb-theme(chat-message-avatar-background-color); @@ -104,6 +105,7 @@ .sender { font-size: 0.875rem; color: nb-theme(chat-message-sender-text-color); + margin-top: 0; margin-bottom: 0.5rem; } @@ -111,6 +113,7 @@ word-break: break-word; white-space: pre-wrap; max-width: 100%; + margin-top: 0; margin-bottom: 0; } @@ -179,6 +182,7 @@ color: nb-theme(chat-message-quote-text-color); padding: 1rem; border-radius: 0.5rem; + margin-top: 0; margin-bottom: 0.5rem; } @@ -218,6 +222,9 @@ flex-direction: row-reverse; .message { + display: flex; + flex-direction: column; + align-items: flex-end; margin-left: 0; @include nb-ltr(margin-right, 0.5rem); @@ -322,5 +329,42 @@ } } } + + .nb-custom-message { + display: inline-block; + padding: nb-theme(chat-padding); + margin-top: 0.5rem; + border-radius: 0.5rem; + } + + .nb-custom-message-full-width { + width: 100%; + } + + .nb-custom-message-no-space { + margin-top: 0; + } + + .nb-custom-message-not-reply { + background: nb-theme(chat-message-background); + color: nb-theme(chat-message-text-color); + + @include nb-ltr(border-top-left-radius, 0); + @include nb-rtl(border-top-right-radius, 0); + + a, + a:hover, + a:focus, { + color: nb-theme(chat-message-text-color); + } + } + + .nb-custom-message-reply { + background: nb-theme(chat-message-reply-background-color); + color: nb-theme(chat-message-reply-text-color); + + @include nb-ltr(border-top-right-radius, 0); + @include nb-rtl(border-top-left-radius, 0); + } } diff --git a/src/framework/theme/components/chat/chat-avatar.component.ts b/src/framework/theme/components/chat/chat-avatar.component.ts new file mode 100644 index 0000000000..f176273547 --- /dev/null +++ b/src/framework/theme/components/chat/chat-avatar.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { SafeStyle } from '@angular/platform-browser'; + +@Component({ + selector: 'nb-chat-avatar', + template: ` + + {{ initials }} + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatAvatarComponent { + + @Input() initials: string; + + @Input() + @HostBinding('style.background-image') + avatarStyle: SafeStyle; + + @HostBinding('class.avatar') + readonly avatarClass = true; +} diff --git a/src/framework/theme/components/chat/chat-custom-message.directive.spec.ts b/src/framework/theme/components/chat/chat-custom-message.directive.spec.ts new file mode 100644 index 0000000000..562b3ea53e --- /dev/null +++ b/src/framework/theme/components/chat/chat-custom-message.directive.spec.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NbChatModule, NbThemeModule } from '@nebular/theme'; +import { NbCustomMessageService } from './custom-message.service'; + +@Component({ + selector: 'nb-custom-message-directive-test', + template: ` +
+

Hello world

+
+ `, +}) +export class NbCustomMessageTestComponent { + customMessageType: string = 'simpleMessageType'; +} + +describe('Directive chat-custom-message-directive: NbCustomMessageTestComponent', () => { + let fixture: ComponentFixture; + let component: NbCustomMessageTestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NbThemeModule.forRoot(), NbChatModule], + declarations: [NbCustomMessageTestComponent], + providers: [NbCustomMessageService], + }); + + fixture = TestBed.createComponent(NbCustomMessageTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/framework/theme/components/chat/chat-custom-message.directive.ts b/src/framework/theme/components/chat/chat-custom-message.directive.ts new file mode 100644 index 0000000000..c9154dbb8a --- /dev/null +++ b/src/framework/theme/components/chat/chat-custom-message.directive.ts @@ -0,0 +1,79 @@ +import { Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; + +import { convertToBoolProperty, NbBooleanInput } from '../helpers'; +import { NbCustomMessageService } from './custom-message.service'; + +function throwCustomMessageTypeIsRequired(): void { + throw new Error('[nbCustomMessage]: custom message type is required.'); +} + +/** + * `[nbCustomMessage]` directive should be used as a structural directive or should be applied to the `ng-template`: + * + * ```html + *
+ * + *
+ * ``` + * or + * ```html + * + * + * + * ``` + */ +@Directive({ + selector: `[nbCustomMessage]`, +}) +export class NbChatCustomMessageDirective implements OnInit, OnDestroy { + + /** + * Defines a message type which should rendered with the custom message template. + * @type {string} + */ + @Input() + get nbCustomMessage(): string { + return this._type; + } + set nbCustomMessage(value: string) { + this._type = value; + } + protected _type: string; + + get type(): string { + return this._type + } + + /** + * Disables generic message styles, such as round corners, text color, background, etc., + * so a custom message could be styled from the ground up. + * + * @type {boolean} + */ + @Input() + set nbCustomMessageNoStyles(value: boolean) { + this._noStyles = convertToBoolProperty(value); + } + get nbCustomMessageNoStyles(): boolean { + return this._noStyles; + } + protected _noStyles: boolean = false; + static ngAcceptInputType_noStyles: NbBooleanInput; + + get noStyles(): boolean { + return this.nbCustomMessageNoStyles; + } + + constructor(public templateRef: TemplateRef, protected customMessageService: NbCustomMessageService) { } + + ngOnInit() { + if (!this._type) { + throwCustomMessageTypeIsRequired(); + } + this.customMessageService.register(this.type, this); + } + + ngOnDestroy() { + this.customMessageService.unregister(this.type); + } +} diff --git a/src/framework/theme/components/chat/chat-message-quote.component.spec.ts b/src/framework/theme/components/chat/chat-message-quote.component.spec.ts new file mode 100644 index 0000000000..a83cb9af99 --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-quote.component.spec.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NbChatModule, NbThemeModule } from '@nebular/theme'; + +@Component({ + selector: 'nb-chat-message-quote-test', + template: ` + + + `, +}) +export class NbChatMessageQuoteTestComponent { + sender: string; + dateFormat: string; + message: string; + date: Date; + quote: string; +} + +describe('Chat-message-quote component: NbChatMessageQuoteTestComponent', () => { + let fixture: ComponentFixture; + let component: NbChatMessageQuoteTestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NbThemeModule.forRoot(), NbChatModule], + declarations: [NbChatMessageQuoteTestComponent], + }); + + fixture = TestBed.createComponent(NbChatMessageQuoteTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('quote should be created', () => { + component.message = 'some test message'; + component.date = new Date(); + component.dateFormat = 'shortTime'; + component.quote = 'quote'; + fixture.detectChanges(); + const quoteElement = fixture.nativeElement.querySelector('.quote'); + expect(quoteElement).toBeTruthy(); + }); + + it('should be created inner chat message', () => { + component.message = 'some test message'; + component.date = new Date(); + component.dateFormat = 'shortTime'; + component.quote = 'quote'; + fixture.detectChanges(); + const quoteElement = fixture.nativeElement.querySelector('nb-chat-message-text'); + expect(quoteElement).toBeTruthy(); + }); + +}); diff --git a/src/framework/theme/components/chat/chat-message-text.component.spec.ts b/src/framework/theme/components/chat/chat-message-text.component.spec.ts new file mode 100644 index 0000000000..35e458be63 --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-text.component.spec.ts @@ -0,0 +1,71 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NbChatModule, NbThemeModule } from '@nebular/theme'; + +@Component({ + selector: 'nb-chat-message-text-test', + template: ` + + + `, +}) +export class NbChatMessageTextTestComponent { + sender: string; + dateFormat: string; + message: string; + date: Date; +} + +describe('Chat-message-text component: NbChatMessageTextTestComponent', () => { + let fixture: ComponentFixture; + let component: NbChatMessageTextTestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NbThemeModule.forRoot(), NbChatModule], + declarations: [NbChatMessageTextTestComponent], + }); + + fixture = TestBed.createComponent(NbChatMessageTextTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should set all inputs', () => { + component.sender = 'AB'; + component.message = 'new text message'; + component.date = new Date(); + component.dateFormat = 'shortTime'; + fixture.detectChanges(); + + const msgValue = fixture.nativeElement.querySelector('.text').textContent; + expect(msgValue).toContain('new text message'); + + const senderInitials = fixture.nativeElement.querySelector('.sender').textContent; + expect(senderInitials).toContain('AB'); + + const time = fixture.nativeElement.querySelector('time'); + expect(time).toBeTruthy(); + }); + + it('should not show sender if it is not provided', () => { + component.message = 'some test message'; + fixture.detectChanges(); + const sender = fixture.nativeElement.querySelector('.sender'); + expect(sender).toBeFalsy(); + }); + + it('should not show message if it is not provided', () => { + component.sender = 'JD'; + fixture.detectChanges(); + const sender = fixture.nativeElement.querySelector('.message'); + expect(sender).toBeFalsy(); + }); +}); diff --git a/src/framework/theme/components/chat/chat-message.component.spec.ts b/src/framework/theme/components/chat/chat-message.component.spec.ts new file mode 100644 index 0000000000..7f13871251 --- /dev/null +++ b/src/framework/theme/components/chat/chat-message.component.spec.ts @@ -0,0 +1,142 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NbChatMessageComponent, NbChatModule, NbThemeModule } from '@nebular/theme'; +import { NbCustomMessageService } from './custom-message.service'; + +@Component({ + selector: 'nb-chat-message-test', + template: ` + + + + + + + + + `, +}) +export class NbChatMessageTestComponent { + messages = []; + + loadMessages(): void { + this.messages = [{ + reply: false, + type: 'link', + optionalData: { + href: 'https://akveo.github.io/ngx-admin/', + label: 'Visit Akveo Nebular', + }, + date: new Date(), + user: { + name: 'Frodo Baggins', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + text: 'Hello, how are you?', + reply: true, + type: 'text', + date: new Date(), + user: { + name: 'Bilbo Baggins', + avatar: '', + }, + }]; + } +} + +describe('Chat-message component: NbChatMessageTestComponent', () => { + let fixture: ComponentFixture; + let component: NbChatMessageTestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, NbThemeModule.forRoot(), NbChatModule], + declarations: [NbChatMessageTestComponent], + }); + + fixture = TestBed.createComponent(NbChatMessageTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should create custom messages with content', () => { + component.loadMessages(); + fixture.detectChanges(); + + const customMessageElement = fixture.nativeElement.querySelector('a'); + expect(customMessageElement).toBeTruthy(); + + const linkHref = customMessageElement.getAttribute('href'); + expect(linkHref).toBe('https://akveo.github.io/ngx-admin/'); + + const linkLabel = customMessageElement.textContent; + expect(linkLabel).toBe('Visit Akveo Nebular'); + }); + + +}); + +describe('Component: NbChatMessageComponent', () => { + let chat: NbChatMessageComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, NbThemeModule.forRoot(), NbChatModule], + providers: [NbCustomMessageService], + }); + + fixture = TestBed.createComponent(NbChatMessageComponent); + chat = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(chat).toBeTruthy(); + }); + + it('ChatMessageComponent testing getInitials method', () => { + chat.sender = ''; + fixture.detectChanges(); + expect(chat.getInitials()).toEqual(''); + + chat.sender = 'John Connor'; + fixture.detectChanges(); + expect(chat.getInitials()).toEqual('JC'); + }); + + it('ChatMessageComponent testing _isDefaultMessageType method', () => { + chat.type = null; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = undefined; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = 'text'; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = 'file'; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = 'map'; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = 'quote'; + expect(chat._isBuiltInMessageType()).toBe(true); + chat.type = 'link'; + expect(chat._isBuiltInMessageType()).toBe(false); + chat.type = 'button'; + expect(chat._isBuiltInMessageType()).toBe(false); + }); +}); diff --git a/src/framework/theme/components/chat/chat-message.component.ts b/src/framework/theme/components/chat/chat-message.component.ts index 2c48d4fda5..daa0ae6047 100644 --- a/src/framework/theme/components/chat/chat-message.component.ts +++ b/src/framework/theme/components/chat/chat-message.component.ts @@ -4,12 +4,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; -import { convertToBoolProperty, NbBooleanInput } from '../helpers'; +import { ChangeDetectionStrategy, Component, HostBinding, Input, TemplateRef } from '@angular/core'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; import { animate, state, style, transition, trigger } from '@angular/animations'; +import { convertToBoolProperty, NbBooleanInput } from '../helpers'; import { NbChatMessageFile } from './chat-message-file.component'; +import { NbCustomMessageService } from './custom-message.service'; +import { NbChatCustomMessageDirective } from './chat-custom-message.directive'; /** * Chat message component. @@ -58,35 +60,62 @@ import { NbChatMessageFile } from './chat-message-file.component'; @Component({ selector: 'nb-chat-message', template: ` -
- - {{ getInitials() }} - -
-
- + + +
+ + [sender]="sender" + [date]="date" + [dateFormat]="dateFormat" + [message]="message" + [files]="files"> + [sender]="sender" + [date]="date" + [dateFormat]="dateFormat" + [message]="message" + [quote]="quote"> + [sender]="sender" + [date]="date" + [message]="message" + [latitude]="latitude" + [longitude]="longitude">
+ + + + +
+ + +
+
`, animations: [ trigger('flyInOut', [ @@ -104,6 +133,21 @@ import { NbChatMessageFile } from './chat-message-file.component'; }) export class NbChatMessageComponent { + protected readonly builtInMessageTypes: string[] = ['text', 'file', 'map', 'quote']; + + avatarStyle: SafeStyle; + + get _addReplyClass(): boolean { + return this._areDefaultStylesEnabled() && this.reply; + } + + get _addNotReplyClass(): boolean { + return this._areDefaultStylesEnabled() && this.notReply; + } + + get _addNoSpaceClass(): boolean { + return this._areDefaultStylesEnabled() && !this.message; + } @HostBinding('@flyInOut') get flyInOut() { @@ -115,8 +159,6 @@ export class NbChatMessageComponent { return !this.reply; } - avatarStyle: SafeStyle; - /** * Determines if a message is a reply */ @@ -193,15 +235,47 @@ export class NbChatMessageComponent { */ @Input() type: string; - constructor(protected domSanitizer: DomSanitizer) { } + /** + * Data which will be set as custom message template context + * @type {any} + */ + @Input() customMessageData: any; + + constructor(protected domSanitizer: DomSanitizer, protected customMessageService: NbCustomMessageService) { } getInitials(): string { if (this.sender) { const names = this.sender.split(' '); - return names.map(n => n.charAt(0)).splice(0, 2).join('').toUpperCase(); } - return ''; } + + _isBuiltInMessageType(): boolean { + // Unset type defaults to "text" type + return this.type == null || this.builtInMessageTypes.includes(this.type); + } + + _getTemplate(): TemplateRef { + const customMessage = this.getCustomMessage(this.type); + return customMessage.templateRef; + } + + _getTemplateContext(): { $implicit: any, isReply: boolean } { + return { $implicit: this.customMessageData, isReply: this.reply }; + } + + _areDefaultStylesEnabled(): boolean { + const customMessageDirective = this.getCustomMessage(this.type); + return !customMessageDirective.noStyles; + } + + protected getCustomMessage(type: string): NbChatCustomMessageDirective { + const customMessageDirective = this.customMessageService.getInstance(type); + if (!customMessageDirective) { + throw new Error(`nb-chat: Can't find template for custom type '${type}'. ` + + `Make sure you provide it in the chat component with *nbCustomMessage='${type}'.`); + } + return customMessageDirective; + } } diff --git a/src/framework/theme/components/chat/chat.component.ts b/src/framework/theme/components/chat/chat.component.ts index 356766318e..ca4c5b96c7 100644 --- a/src/framework/theme/components/chat/chat.component.ts +++ b/src/framework/theme/components/chat/chat.component.ts @@ -25,6 +25,7 @@ import { NbComponentOrCustomStatus } from '../component-status'; import { convertToBoolProperty, NbBooleanInput } from '../helpers'; import { NbChatFormComponent } from './chat-form.component'; import { NbChatMessageComponent } from './chat-message.component'; +import { NbCustomMessageService } from './custom-message.service'; /** * Conversational UI collection - a set of components for chat-like UI construction. @@ -111,6 +112,75 @@ import { NbChatMessageComponent } from './chat-message.component'; * * @stacked-example(Chat Sizes, chat/chat-sizes.component) * + * # Custom message types + * + * Besides built-in message types, you could provide custom ones with their own template to render. + * As an example, let's add the `link` message type. + *
+ * First, you need to provide a template for the `link` message type: + * ```html + * + * example.com + * + * ``` + * Then, add the `nb-chat-message` component with the `link` type: + * ```html + * + * example.com + * + * + * ``` + * + *
+ *
Important!
+ *
+ * Custom chat messages must be defined before the `nb-chat-message`. + *
+ *
+ * + * Custom message templates could have arbitrary data associated with them. Let's extract hardcoded link + * href and text. To pass some data to the custom message template, use the `customMessageData` input + * of the `nb-chat-message` component: + * ```html + * ... + * + * + * ... + * ``` + * When `customMessageData` is set, this object would become a template context and you'll be able + * to reference it via `let varName` syntax: + * ```html + * {{ data.text }} + * ``` + * + * That's it, full example will look like this: + * ```html + * + * {{ data.text }} + * + * + * + * ``` + * + * If you want to style your custom template from the ground up you could turn off generic message styling + * (such as round borders, color, background, etc.) via the `noStyles` input: + * ```html + *
...
+ * ``` + * When you decide to use your own styles, the `isReply` property of the custom message template context + * would come in handy. This property allows you to determine whether the message is a reply or not. + * For example, to change link text color (as replies have a different background): + * ```html + * + * {{ data.label }} + * + * ``` + * + * Below, you could find a more complex example with multiple custom message types: + * @stacked-example(Custom message, chat/chat-custom-message.component) + * * @styles * * chat-background-color: @@ -178,6 +248,9 @@ import { NbChatMessageComponent } from './chat-message.component';
`, + providers: [ + NbCustomMessageService, + ], }) export class NbChatComponent implements OnChanges, AfterContentInit, AfterViewInit { diff --git a/src/framework/theme/components/chat/chat.module.ts b/src/framework/theme/components/chat/chat.module.ts index e50192b348..cf2c91969b 100644 --- a/src/framework/theme/components/chat/chat.module.ts +++ b/src/framework/theme/components/chat/chat.module.ts @@ -19,6 +19,8 @@ import { NbChatMessageFileComponent } from './chat-message-file.component'; import { NbChatMessageQuoteComponent } from './chat-message-quote.component'; import { NbChatMessageMapComponent } from './chat-message-map.component'; import { NbChatOptions } from './chat.options'; +import { NbChatAvatarComponent } from './chat-avatar.component'; +import { NbChatCustomMessageDirective } from './chat-custom-message.directive'; const NB_CHAT_COMPONENTS = [ NbChatComponent, @@ -28,6 +30,11 @@ const NB_CHAT_COMPONENTS = [ NbChatMessageFileComponent, NbChatMessageQuoteComponent, NbChatMessageMapComponent, + NbChatAvatarComponent, +]; + +const NB_CHAT_DIRECTIVES = [ + NbChatCustomMessageDirective, ]; @NgModule({ @@ -39,9 +46,11 @@ const NB_CHAT_COMPONENTS = [ ], declarations: [ ...NB_CHAT_COMPONENTS, + ...NB_CHAT_DIRECTIVES, ], exports: [ ...NB_CHAT_COMPONENTS, + ...NB_CHAT_DIRECTIVES, ], }) export class NbChatModule { diff --git a/src/framework/theme/components/chat/custom-message.service.ts b/src/framework/theme/components/chat/custom-message.service.ts new file mode 100644 index 0000000000..37ffe73809 --- /dev/null +++ b/src/framework/theme/components/chat/custom-message.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; + +import { NbChatCustomMessageDirective } from './chat-custom-message.directive'; + +/** + * `NbCustomMessageService` is used to store instances of `NbChatCustomMessageDirective`s which + * were provided in the chat component. + */ +@Injectable() +export class NbCustomMessageService { + + protected readonly customMessages = new Map(); + + register(type: string, instance: NbChatCustomMessageDirective): void { + this.customMessages.set(type, instance); + } + + unregister(type: string): boolean { + return this.customMessages.delete(type); + } + + getInstance(type: string): NbChatCustomMessageDirective | undefined { + return this.customMessages.get(type); + } +} diff --git a/src/framework/theme/public_api.ts b/src/framework/theme/public_api.ts index e339fdf8d4..4ea739377d 100644 --- a/src/framework/theme/public_api.ts +++ b/src/framework/theme/public_api.ts @@ -109,6 +109,8 @@ export * from './components/chat/chat-message-text.component'; export * from './components/chat/chat-form.component'; export * from './components/chat/chat.module'; export * from './components/chat/chat.options'; +export * from './components/chat/chat-avatar.component'; +export * from './components/chat/chat-custom-message.directive'; export * from './components/spinner/spinner.component'; export * from './components/spinner/spinner.directive'; export * from './components/spinner/spinner.module'; diff --git a/src/playground/with-layout/chat/chat-custom-message.component.html b/src/playground/with-layout/chat/chat-custom-message.component.html new file mode 100644 index 0000000000..ee0ec8cfce --- /dev/null +++ b/src/playground/with-layout/chat/chat-custom-message.component.html @@ -0,0 +1,32 @@ + + + + {{ data.text }} + + +
+

Wow! Button in a message!

+ +
+ + + + + + + + + + +
diff --git a/src/playground/with-layout/chat/chat-custom-message.component.scss b/src/playground/with-layout/chat/chat-custom-message.component.scss new file mode 100644 index 0000000000..2406f557c2 --- /dev/null +++ b/src/playground/with-layout/chat/chat-custom-message.component.scss @@ -0,0 +1,13 @@ +nb-chat { + width: 500px; + margin: 0 auto; +} + +img { + width: 100%; + height: auto; +} + +.button-custom-message-text { + margin-top: 0; +} diff --git a/src/playground/with-layout/chat/chat-custom-message.component.ts b/src/playground/with-layout/chat/chat-custom-message.component.ts new file mode 100644 index 0000000000..3e2c8dc16e --- /dev/null +++ b/src/playground/with-layout/chat/chat-custom-message.component.ts @@ -0,0 +1,100 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; + +@Component({ + templateUrl: './chat-custom-message.component.html', + styleUrls: ['./chat-custom-message.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChatCustomMessageComponent implements OnInit { + + readonly tableData = { + columns: [ 'First Name', 'Last Name', 'Age' ], + rows: [ + { firstName: 'Robert', lastName: 'Baratheon', age: 46 }, + { firstName: 'Jaime', lastName: 'Lannister', age: 31 }, + ], + }; + + messages: any[] = []; + + ngOnInit(): void { + this.loadMessages(); + } + + sendMessage(event: any): void { + this.messages.push({ + text: event.message, + date: new Date(), + reply: true, + type: 'text', + user: { + name: 'Gandalf the Grey', + avatar: 'https://i.gifer.com/no.gif', + }, + }); + } + + private loadMessages(): void { + this.messages = [ + { + type: 'link', + text: 'Now you able to use links!', + customMessageData: { + href: 'https://akveo.github.io/nebular/', + text: 'Go to Nebular', + }, + reply: false, + date: new Date(), + user: { + name: 'Frodo Baggins', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + type: 'link', + customMessageData: { + href: 'https://akveo.github.io/ngx-admin/', + text: 'Go to ngx-admin', + }, + reply: true, + date: new Date(), + user: { + name: 'Meriadoc Brandybuck', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + type: 'button', + customMessageData: 'Click to scroll down', + reply: false, + date: new Date(), + user: { + name: 'Gimli Gloin', + avatar: '', + }, + }, + { + type: 'table', + text: `Now let's try to add a table`, + customMessageData: this.tableData, + reply: false, + date: new Date(), + user: { + name: 'Fredegar Bolger', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + type: 'table', + text: `And one more table but now in the reply`, + customMessageData: this.tableData, + reply: true, + date: new Date(), + user: { + name: 'Fredegar Bolger', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + ] + } +} diff --git a/src/playground/with-layout/chat/chat-routing.module.ts b/src/playground/with-layout/chat/chat-routing.module.ts index f8bd722da5..d824e6a760 100644 --- a/src/playground/with-layout/chat/chat-routing.module.ts +++ b/src/playground/with-layout/chat/chat-routing.module.ts @@ -13,6 +13,7 @@ import { ChatMessageTypesShowcaseComponent } from './chat-message-types-showcase import { ChatShowcaseComponent } from './chat-showcase.component'; import { ChatSizesComponent } from './chat-sizes.component'; import { ChatTestComponent } from './chat-test.component'; +import { ChatCustomMessageComponent } from './chat-custom-message.component'; const routes: Route[] = [ { @@ -43,6 +44,10 @@ const routes: Route[] = [ path: 'chat-test.component', component: ChatTestComponent, }, + { + path: 'chat-custom-message.component', + component: ChatCustomMessageComponent, + }, ]; @NgModule({ diff --git a/src/playground/with-layout/chat/chat.module.ts b/src/playground/with-layout/chat/chat.module.ts index e379a6f8af..7e925108ff 100644 --- a/src/playground/with-layout/chat/chat.module.ts +++ b/src/playground/with-layout/chat/chat.module.ts @@ -6,7 +6,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { NbCardModule, NbChatModule } from '@nebular/theme'; +import { NbCardModule, NbChatModule, NbButtonModule } from '@nebular/theme'; + import { ChatRoutingModule } from './chat-routing.module'; import { ChatColorsComponent } from './chat-colors.component'; import { ChatConversationShowcaseComponent } from './chat-conversation-showcase.component'; @@ -15,6 +16,8 @@ import { ChatMessageTypesShowcaseComponent } from './chat-message-types-showcase import { ChatShowcaseComponent } from './chat-showcase.component'; import { ChatSizesComponent } from './chat-sizes.component'; import { ChatTestComponent } from './chat-test.component'; +import { ChatCustomMessageComponent } from './chat-custom-message.component'; +import { ChatCustomMessageTableComponent } from './components/chat-custom-message-table.component'; @NgModule({ declarations: [ @@ -25,11 +28,15 @@ import { ChatTestComponent } from './chat-test.component'; ChatShowcaseComponent, ChatSizesComponent, ChatTestComponent, + ChatCustomMessageComponent, + ChatCustomMessageTableComponent, ], imports: [ CommonModule, NbChatModule.forRoot(), NbCardModule, - ChatRoutingModule ], + NbButtonModule, + ChatRoutingModule, + ], }) export class ChatModule {} diff --git a/src/playground/with-layout/chat/components/chat-custom-message-table.component.scss b/src/playground/with-layout/chat/components/chat-custom-message-table.component.scss new file mode 100644 index 0000000000..79a59452f1 --- /dev/null +++ b/src/playground/with-layout/chat/components/chat-custom-message-table.component.scss @@ -0,0 +1,25 @@ +@import '../../../styles/themes'; + +:host { + display: block; + margin-top: 0.5rem; + padding: 1rem; + border-radius: 0.5rem; +} + +:host(.reply) { + background: nb-theme(background-basic-color-2); + @include nb-ltr(border-top-right-radius, 0); + @include nb-rtl(border-top-left-radius, 0); +} + +:host(:not(.reply)) { + background: nb-theme(background-primary-color-1); + color: nb-theme(text-control-color); + @include nb-ltr(border-top-left-radius, 0); + @include nb-rtl(border-top-right-radius, 0); +} + +table { + width: 100%; +} diff --git a/src/playground/with-layout/chat/components/chat-custom-message-table.component.ts b/src/playground/with-layout/chat/components/chat-custom-message-table.component.ts new file mode 100644 index 0000000000..1d5268fdcf --- /dev/null +++ b/src/playground/with-layout/chat/components/chat-custom-message-table.component.ts @@ -0,0 +1,33 @@ +import { Component, HostBinding, Input } from '@angular/core'; + +@Component({ + selector: 'nb-custom-message-table', + template: ` + + + + + + + + + + + + + + +
{{ column }}
{{ row.firstName }}{{ row.lastName }}{{ row.age }}
+ `, + styleUrls: ['./chat-custom-message-table.component.scss'], +}) +export class ChatCustomMessageTableComponent { + + @Input() columns = []; + + @Input() rows = []; + + @Input() + @HostBinding('class.reply') + isReply: boolean; +}