diff --git a/docs/app/app.module.ts b/docs/app/app.module.ts index 1ee5428360..5e2756bb09 100644 --- a/docs/app/app.module.ts +++ b/docs/app/app.module.ts @@ -8,6 +8,7 @@ import { InjectionToken, NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NbThemeModule, NbSidebarModule, @@ -29,6 +30,7 @@ const docs = require('../output.json'); @NgModule({ imports: [ BrowserModule, + BrowserAnimationsModule, FormsModule, HttpClientModule, NbSidebarModule, diff --git a/docs/assets/images/components/chat-ui.svg b/docs/assets/images/components/chat-ui.svg new file mode 100644 index 0000000000..d92b67b76c --- /dev/null +++ b/docs/assets/images/components/chat-ui.svg @@ -0,0 +1,26 @@ + + + + 99B9075A-1274-4500-A235-3106F3DC93C5 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/structure.ts b/docs/structure.ts index 7bd7852e05..0a66ef0b88 100644 --- a/docs/structure.ts +++ b/docs/structure.ts @@ -214,6 +214,16 @@ export const structure = [ 'NbRouteTabsetComponent', ], }, + { + type: 'tabs', + name: 'Chat UI', + icon: 'chat-ui.svg', + source: [ + 'NbChatComponent', + 'NbChatMessageComponent', + 'NbChatFormComponent', + ], + }, { type: 'tabs', name: 'Actions', diff --git a/e2e/chat.e2e-spec.ts b/e2e/chat.e2e-spec.ts new file mode 100644 index 0000000000..9313f559b5 --- /dev/null +++ b/e2e/chat.e2e-spec.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { browser, element, by } from 'protractor'; +import { colors, chatSizes as sizes } from './component-shared'; +import { waitFor } from './e2e-helper'; + +let chats: any[] = []; + +function prepareChats() { + const result: any[] = []; + + let elementNumber: number = 1; + for (const { colorKey, color } of colors) { + for (const { sizeKey, height } of sizes) { + result.push({ + size: sizeKey, + height: height, + colorKey, + color, + elementNumber, + }); + elementNumber++; + } + } + + return result; +} + +describe('nb-chat', () => { + + chats = prepareChats(); + + beforeEach((done) => { + browser.get('#/chat/chat-test.component').then(() => done()); + }); + + chats.forEach(c => { + + it(`should display ${c.colorKey} chat with ${c.size} size`, () => { + waitFor(`nb-chat:nth-child(${c.elementNumber})`); + + element(by.css(`nb-chat:nth-child(${c.elementNumber})`)).getCssValue('height').then(height => { + expect(height).toEqual(c.height); + }); + + element(by.css(`nb-chat:nth-child(${c.elementNumber}) .header`)) + .getCssValue('background-color').then(bgColor => { + expect(bgColor).toEqual(c.color); + }); + }); + }); + + it('should add on message', () => { + const all: any = element.all(by.css('nb-chat:nth-child(1) nb-chat-message')); + all.count().then(allCount => { + element(by.css('nb-chat:nth-child(1) nb-chat-form input')).sendKeys('akveo'); + element(by.css('nb-chat:nth-child(1) nb-chat-form .btn')).click(); + expect(all.count()).toEqual(allCount + 1); + }); + }); + + it('should not add on an empty message', () => { + const all: any = element.all(by.css('nb-chat:nth-child(1) nb-chat-message')); + all.count().then(allCount => { + element(by.css('nb-chat:nth-child(1) nb-chat-form .btn')).click(); + expect(all.count()).toEqual(allCount); + + element(by.css('nb-chat:nth-child(1) nb-chat-form input')).sendKeys(' '); + element(by.css('nb-chat:nth-child(1) nb-chat-form .btn')).click(); + expect(all.count()).toEqual(allCount); + }); + }); +}); diff --git a/e2e/component-shared.ts b/e2e/component-shared.ts index d4219ed931..b6cffb41d9 100644 --- a/e2e/component-shared.ts +++ b/e2e/component-shared.ts @@ -29,3 +29,5 @@ export const colors = [ { colorKey: 'default', color: hexToRgbA('#a4abb3') }, { colorKey: 'disabled', color: 'rgba(255, 255, 255, 0.4)' }, ]; + +export const chatSizes = cardSizes; diff --git a/package-lock.json b/package-lock.json index 92cc0f7649..c3f9e8d899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6878,12 +6878,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6898,17 +6900,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7025,7 +7030,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7037,6 +7043,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7051,6 +7058,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7058,12 +7066,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -7082,6 +7092,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7162,7 +7173,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7174,6 +7186,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7295,6 +7308,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0157f2a77a..adb1ed18b0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -9,15 +9,19 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { HttpClientModule } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NbThemeModule } from '@nebular/theme'; import { NbAppComponent } from './app.component'; import { NbLayoutDirectionToggleComponent } from './layout-direction-toggle/layout-direction-toggle.component'; import { NbDynamicToAddComponent } from '../playground/shared/dynamic.component'; import { NbPlaygroundSharedModule } from '../playground/shared/shared.module'; + + @NgModule({ imports: [ BrowserModule, + BrowserAnimationsModule, FormsModule, HttpClientModule, RouterModule.forRoot([ diff --git a/src/framework/theme/components/chat/_chat.component.theme.scss b/src/framework/theme/components/chat/_chat.component.theme.scss new file mode 100644 index 0000000000..99b305b5ab --- /dev/null +++ b/src/framework/theme/components/chat/_chat.component.theme.scss @@ -0,0 +1,402 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +@mixin nb-chat-theme() { + + nb-chat { + font-size: nb-theme(chat-font-size); + background: nb-theme(chat-bg); + border-radius: nb-theme(chat-border-radius); + box-shadow: nb-theme(chat-shadow); + + .header { + color: nb-theme(chat-fg-text); + padding: nb-theme(chat-padding); + border-bottom: 1px solid nb-theme(chat-separator); + border-top-left-radius: nb-theme(chat-border-radius); + border-top-right-radius: nb-theme(chat-border-radius); + font-weight: nb-theme(font-weight-bolder); + } + + .scrollable { + overflow: auto; + flex: 1; + @include nb-scrollbars( + nb-theme(scrollbar-fg), + nb-theme(scrollbar-bg), + nb-theme(scrollbar-width)); + } + + .messages { + padding: nb-theme(chat-padding); + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-shrink: 0; + flex-direction: column; + } + + .no-messages { + font-size: 0.875rem; + text-align: center; + } + + nb-chat-message { + margin-bottom: 1.5rem; + display: flex; + flex-direction: row; + + .message { + flex: 1; + } + + .avatar { + border-radius: 50%; + flex-shrink: 0; + background: nb-theme(chat-message-avatar-bg); + background-position: center; + background-size: 3.4rem 2.6rem; + background-repeat: no-repeat; + width: 2.5rem; + height: 2.5rem; + text-align: center; + line-height: 2.5rem; + font-size: 0.875rem; + color: white; + } + + nb-chat-message-text { + + display: flex; + flex-direction: column; + + .sender { + font-size: 0.875rem; + color: nb-theme(chat-message-sender-fg); + margin-bottom: 0.5rem; + } + + p { + word-wrap: break-word; + word-break: break-all; + max-width: 100%; + margin-bottom: 0; + } + + .text { + padding: 1rem; + border-radius: 0.5rem; + } + } + + nb-chat-message-file { + display: flex; + flex-direction: column; + + a { + color: nb-theme(chat-message-file-fg); + background: nb-theme(chat-message-file-bg); + font-size: 4rem; + text-align: center; + border: 1px solid nb-theme(chat-message-file-fg); + width: 10rem; + height: 10rem; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 0.5rem; + &:hover, &:focus { + text-decoration: none; + color: nb-theme(chat-message-file-fg); + } + div { + background-size: cover; + width: 100%; + height: 100%; + } + } + + nb-chat-message-text { + display: block; + margin-bottom: 0.5rem; + } + + .message-content-group { + display: flex; + flex-direction: row; + justify-content: flex-end; + flex-wrap: wrap; + + a { + @include nb-ltr(margin-right, 1rem); + @include nb-rtl(margin-left, 1rem); + margin-bottom: 1rem; + width: 5rem; + height: 5rem; + } + } + } + + nb-chat-message-quote { + + p.quote { + font-style: italic; + font-size: 0.875rem; + background: nb-theme(chat-message-quote-bg); + color: nb-theme(chat-message-quote-fg); + padding: 1rem; + border-radius: 0.5rem; + margin-bottom: 0.5rem; + } + + .sender { + font-size: 0.875rem; + color: nb-theme(chat-message-sender-fg); + margin-bottom: 0.5rem; + } + } + + &.not-reply { + .message { + @include nb-ltr(margin-left, 0.5rem); + @include nb-rtl(margin-right, 0.5rem); + + @include nb-ltr(margin-right, 3rem); + @include nb-rtl(margin-left, 3rem); + } + + nb-chat-message-text { + align-items: flex-start; + .text { + @include nb-ltr(border-top-left-radius, 0); + @include nb-rtl(border-top-right-radius, 0); + background: nb-theme(chat-message-bg); + color: nb-theme(chat-message-fg); + } + } + + nb-chat-message-file { + align-items: flex-start; + } + } + + &.reply { + flex-direction: row-reverse; + + .message { + margin-left: 0; + + @include nb-ltr(margin-right, 0.5rem); + @include nb-rtl(margin-left, 0.5rem); + + @include nb-ltr(margin-left, 3rem); + @include nb-rtl(margin-right, 3rem); + } + + nb-chat-message-text { + align-items: flex-end; + .sender { + @include nb-ltr(text-align, right); + @include nb-rtl(text-align, left); + } + + .text { + @include nb-ltr(border-top-right-radius, 0); + @include nb-rtl(border-top-left-radius, 0); + background: nb-theme(chat-message-reply-bg); + color: nb-theme(chat-message-reply-fg); + } + } + + nb-chat-message-file { + align-items: flex-end; + } + } + } + + nb-chat-form { + display: flex; + flex-direction: column; + padding: nb-theme(chat-padding); + border-top: 1px solid nb-theme(chat-separator); + + .message-row { + flex-direction: row; + display: flex; + } + + input { + flex: 1; + padding: 1.25rem 1.5rem; + border-radius: 2rem; + border: 1px solid nb-theme(chat-form-border); + background: nb-theme(chat-form-bg); + color: nb-theme(chat-form-fg); + outline: none; + box-sizing: border-box; + + &.with-button { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + @include nb-ltr(border-bottom-right-radius, 0); + @include nb-ltr(border-top-right-radius, 0); + @include nb-rtl(border-bottom-left-radius, 0); + @include nb-rtl(border-top-left-radius, 0); + } + + &::placeholder { + color: nb-theme(chat-form-placeholder-fg); + } + } + + button.btn { + border-radius: 3rem; + @include nb-ltr(border-bottom-left-radius, 0); + @include nb-ltr(border-top-left-radius, 0); + @include nb-rtl(border-bottom-right-radius, 0); + @include nb-rtl(border-top-right-radius, 0); + padding: 0 1.5rem; + + &.with-icon { + font-size: 3rem; + line-height: 1; + padding: 0 1.25rem 0 0.875rem; + text-align: center; + } + } + + &.file-over input { + border: 1px dashed nb-theme(chat-form-active-border); + box-shadow: 0 0 0 4px nb-theme(chat-form-bg); + &::placeholder { + color: nb-theme(chat-form-fg); + } + } + + .dropped-files { + display: flex; + flex-direction: row; + margin-bottom: 0.5rem; + flex-wrap: wrap; + div { + background-size: cover; + width: 3rem; + height: 3rem; + border-radius: 0.5rem; + @include nb-ltr(margin-right, 0.5rem); + @include nb-rtl(margin-left, 0.5rem); + margin-bottom: 0.5rem; + border: 1px solid nb-theme(chat-form-fg); + text-align: center; + line-height: 3rem; + font-size: 2rem; + color: nb-theme(chat-form-fg); + position: relative; + + .remove { + position: absolute; + right: -0.5rem; + top: -0.875rem; + font-size: 0.875rem; + line-height: 1; + cursor: pointer; + } + } + } + } + + &.xxsmall-chat { + height: nb-theme(chat-height-xxsmall); + } + &.xsmall-chat { + height: nb-theme(chat-height-xsmall); + } + &.small-chat { + height: nb-theme(chat-height-small); + } + &.medium-chat { + height: nb-theme(chat-height-medium); + } + &.large-chat { + height: nb-theme(chat-height-large); + } + &.xlarge-chat { + height: nb-theme(chat-height-xlarge); + } + &.xxlarge-chat { + height: nb-theme(chat-height-xxlarge); + } + + &.active-chat { + .header { + background-color: nb-theme(chat-active-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-active-bg); + } + } + &.disabled-chat { + .header { + background-color: nb-theme(chat-disabled-bg); + color: nb-theme(chat-disabled-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-disabled-bg); + border: 1px solid nb-theme(chat-form-border); + color: nb-theme(chat-disabled-fg); + } + } + &.primary-chat { + .header { + background-color: nb-theme(chat-primary-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-primary-bg); + } + } + &.info-chat { + .header { + background-color: nb-theme(chat-info-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-info-bg); + } + } + &.success-chat { + .header { + background-color: nb-theme(chat-success-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-success-bg); + } + } + &.warning-chat { + .header { + background-color: nb-theme(chat-warning-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-warning-bg); + } + } + &.danger-chat { + .header { + background-color: nb-theme(chat-danger-bg); + color: nb-theme(chat-fg); + } + nb-chat-form button.btn { + background-color: nb-theme(chat-danger-bg); + } + } + } +} + diff --git a/src/framework/theme/components/chat/chat-form.component.ts b/src/framework/theme/components/chat/chat-form.component.ts new file mode 100644 index 0000000000..1399b240e2 --- /dev/null +++ b/src/framework/theme/components/chat/chat-form.component.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + HostBinding, + HostListener, + Input, + Output, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** + * Chat form component. + * + * Show a message form with a send message button. + * + * ```ts + * + * + * ``` + * + * When `[dropFiles]="true"` handles files drag&drop with a file preview. + * + * Drag & drop available for files and images: + * @stacked-example(Drag & Drop Chat, chat/chat-drop.component) + * + * New message could be tracked outside by using `(send)` output. + * + * ```ts + * + * + * + * // ... + * + * onNewMessage({ message: string, files: any[] }) { + * this.service.sendToServer(message, files); + * } + * ``` + * + * @styles + * + * chat-form-bg: + * chat-form-fg: + * chat-form-border: + * chat-form-active-border: + * + */ +@Component({ + selector: 'nb-chat-form', + template: ` +
+ +
+ × +
+
+ × +
+
+
+
+ + +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatFormComponent { + + droppedFiles: any[] = []; + imgDropTypes = ['image/png', 'image/jpeg', 'image/gif']; + + /** + * Predefined message text + * @type {string} + */ + @Input() message: string = ''; + + /** + * Send button title + * @type {string} + */ + @Input() buttonTitle: string = ''; + + /** + * Send button icon, shown if `buttonTitle` is empty + * @type {string} + */ + @Input() buttonIcon: string = 'nb-paper-plane'; + + /** + * Show send button + * @type {boolean} + */ + @Input() showButton: boolean = true; + + /** + * Show send button + * @type {boolean} + */ + @Input() dropFiles: boolean = false; + + /** + * + * @type {EventEmitter<{ message: string, files: File[] }>} + */ + @Output() send = new EventEmitter<{ message: string, files: File[] }>(); + + @HostBinding('class.file-over') fileOver = false; + + constructor(private cd: ChangeDetectorRef, private domSanitizer: DomSanitizer) { + } + + @HostListener('drop', ['$event']) + onDrop(event: any) { + if (this.dropFiles) { + event.preventDefault(); + event.stopPropagation(); + + this.fileOver = false; + if (event.dataTransfer && event.dataTransfer.files) { + + // tslint:disable-next-line + for (let file of event.dataTransfer.files) { + const res = file; + + if (this.imgDropTypes.includes(file.type)) { + const fr = new FileReader(); + fr.onload = (e: any) => { + res.src = e.target.result; + res.urlStyle = this.domSanitizer.bypassSecurityTrustStyle(`url(${res.src})`); + this.cd.detectChanges(); + }; + + fr.readAsDataURL(file); + } + this.droppedFiles.push(res); + } + } + } + } + + removeFile(file) { + const index = this.droppedFiles.indexOf(file); + if (index >= 0) { + this.droppedFiles.splice(index, 1); + } + } + + @HostListener('dragover') + onDragOver() { + if (this.dropFiles) { + this.fileOver = true; + } + } + + @HostListener('dragleave') + onDragLeave() { + if (this.dropFiles) { + this.fileOver = false; + } + } + + sendMessage() { + if (this.droppedFiles.length || String(this.message).trim().length) { + this.send.emit({ message: this.message, files: this.droppedFiles }); + this.message = ''; + this.droppedFiles = []; + } + } +} diff --git a/src/framework/theme/components/chat/chat-message-file.component.ts b/src/framework/theme/components/chat/chat-message-file.component.ts new file mode 100644 index 0000000000..6d8cdd1f67 --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-file.component.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +/** + * Chat message component. + * + * @styles + * + */ +@Component({ + selector: 'nb-chat-message-file', + template: ` + + {{ message }} + + + +
+ + +
+
+
+
+ + + + +
+
+
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatMessageFileComponent { + + readyFiles: any[]; + + /** + * Message sender + * @type {string} + */ + @Input() message: string; + + /** + * Message sender + * @type {string} + */ + @Input() sender: string; + + /** + * Message send date + * @type {Date} + */ + @Input() date: Date; + + /** + * Message file path + * @type {Date} + */ + @Input() + set files(files: any[]) { + this.readyFiles = (files || []).map((file: any) => { + const isImage = this.isImage(file); + return { + ...file, + urlStyle: isImage && this.domSanitizer.bypassSecurityTrustStyle(`url(${file.url})`), + isImage: isImage, + }; + }); + this.cd.detectChanges(); + } + + constructor(private cd: ChangeDetectorRef, private domSanitizer: DomSanitizer) { + } + + + isImage(file: any): boolean { + return ['image/png', 'image/jpeg', 'image/gif'].includes(file.type); + } +} diff --git a/src/framework/theme/components/chat/chat-message-map.component.ts b/src/framework/theme/components/chat/chat-message-map.component.ts new file mode 100644 index 0000000000..e55ee24e0c --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-map.component.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { NbChatOptions } from './chat.options'; + +/** + * Chat message component. + * + * @styles + * + */ +@Component({ + selector: 'nb-chat-message-map', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatMessageMapComponent { + + /** + * Message sender + * @type {string} + */ + @Input() message: string; + + /** + * Message sender + * @type {string} + */ + @Input() sender: string; + + /** + * Message send date + * @type {Date} + */ + @Input() date: Date; + + /** + * Map latitude + * @type {number} + */ + @Input() latitude: number; + + /** + * Map longitude + * @type {number} + */ + @Input() longitude: number; + + get file() { + return { + // tslint:disable-next-line + url: `https://maps.googleapis.com/maps/api/staticmap?center=${this.latitude},${this.longitude}&zoom=12&size=400x400&key=${this.mapKey}`, + type: 'image/png', + icon: 'nb-location', + }; + } + + mapKey: string; + + constructor(options: NbChatOptions) { + this.mapKey = options.messageGoogleMapKey; + } +} diff --git a/src/framework/theme/components/chat/chat-message-quote.component.ts b/src/framework/theme/components/chat/chat-message-quote.component.ts new file mode 100644 index 0000000000..610f9a24eb --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-quote.component.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * Chat message component. + * + * @styles + * + */ +@Component({ + selector: 'nb-chat-message-quote', + template: ` +

{{ sender }}

+

+ {{ quote }} +

+ + {{ message }} + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatMessageQuoteComponent { + + /** + * Message sender + * @type {string} + */ + @Input() message: string; + + /** + * Message sender + * @type {string} + */ + @Input() sender: string; + + /** + * Message send date + * @type {Date} + */ + @Input() date: Date; + + /** + * Quoted message + * @type {Date} + */ + @Input() quote: string; + +} diff --git a/src/framework/theme/components/chat/chat-message-text.component.ts b/src/framework/theme/components/chat/chat-message-text.component.ts new file mode 100644 index 0000000000..a290948784 --- /dev/null +++ b/src/framework/theme/components/chat/chat-message-text.component.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +/** + * Chat message component. + * + * @styles + * + */ +@Component({ + selector: 'nb-chat-message-text', + template: ` +

{{ sender }}

+

{{ message }}

+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatMessageTextComponent { + + /** + * Message sender + * @type {string} + */ + @Input() sender: string; + + /** + * Message sender + * @type {string} + */ + @Input() message: string; + + /** + * Message send date + * @type {Date} + */ + @Input() date: Date; + +} diff --git a/src/framework/theme/components/chat/chat-message.component.ts b/src/framework/theme/components/chat/chat-message.component.ts new file mode 100644 index 0000000000..a7a61b5a5c --- /dev/null +++ b/src/framework/theme/components/chat/chat-message.component.ts @@ -0,0 +1,194 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * 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 } from '../helpers'; +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; +import { animate, state, style, transition, trigger } from '@angular/animations'; + +/** + * Chat message component. + * + * Multiple message types are available through a `type` property, such as + * - text - simple text message + * - file - could be a file preview or a file icon + * if multiple files are provided grouped files are shown + * - quote - quotes a message with specific quote styles + * - map - shows a google map picture by provided [latitude] and [longitude] properties + * + * @stacked-example(Available Types, chat/chat-message-types-showcase.component) + * + * Message with attached files: + * ```html + * + * + * ``` + * + * Map message: + * ```html + * + * + * ``` + * + * @styles + * + * chat-message-fg: + * chat-message-bg: + * chat-message-reply-bg: + * chat-message-reply-fg: + * chat-message-avatar-bg: + * chat-message-sender-fg: + * chat-message-quote-fg: + * chat-message-quote-bg: + * chat-message-file-fg: + * chat-message-file-bg: + */ +@Component({ + selector: 'nb-chat-message', + template: ` +
+ + {{ getInitials() }} + +
+
+ + + + + + + + + + + + + + +
+ `, + animations: [ + trigger('flyInOut', [ + state('in', style({ transform: 'translateX(0)' })), + transition('void => *', [ + style({ transform: 'translateX(-100%)' }), + animate(80), + ]), + transition('* => void', [ + animate(80, style({ transform: 'translateX(100%)' })), + ]), + ]), + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbChatMessageComponent { + + + @HostBinding('@flyInOut') + get flyInOut() { + return true; + } + + @HostBinding('class.reply') + replyValue: boolean = false; + + @HostBinding('class.not-reply') + get notReply() { + return !this.replyValue; + } + + avatarStyle: SafeStyle; + + /** + * Determines if a message is a reply + */ + @Input() + set reply(val: boolean) { + this.replyValue = convertToBoolProperty(val); + } + + /** + * Message sender + * @type {string} + */ + @Input() message: string; + + /** + * Message sender + * @type {string} + */ + @Input() sender: string; + + /** + * Message send date + * @type {Date} + */ + @Input() date: Date; + + /** + * Array of files `{ url: 'file url', icon: 'file icon class' }` + * @type {string} + */ + @Input() files: { url: string, icon: string }[]; + + /** + * Quoted message text + * @type {string} + */ + @Input() quote: string; + + /** + * Map latitude + * @type {number} + */ + @Input() latitude: number; + + /** + * Map longitude + * @type {number} + */ + @Input() longitude: number; + + /** + * Message send avatar + * @type {string} + */ + @Input() + set avatar(value: string) { + this.avatarStyle = value ? this.domSanitizer.bypassSecurityTrustStyle(`url(${value})`) : null; + } + + /** + * Message type, available options `text|file|map|quote` + * @type {string} + */ + @Input() type: string; + + constructor(private domSanitizer: DomSanitizer) { } + + getInitials(): string { + if (this.sender) { + const names = this.sender.split(' '); + + return names.map(n => n.charAt(0)).splice(0, 2).join('').toUpperCase(); + } + + return ''; + } +} diff --git a/src/framework/theme/components/chat/chat.component.scss b/src/framework/theme/components/chat/chat.component.scss new file mode 100644 index 0000000000..e34264ce7d --- /dev/null +++ b/src/framework/theme/components/chat/chat.component.scss @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +:host { + display: flex; + flex-direction: column; + position: relative; + height: 100%; +} diff --git a/src/framework/theme/components/chat/chat.component.ts b/src/framework/theme/components/chat/chat.component.ts new file mode 100644 index 0000000000..15d0f87bd9 --- /dev/null +++ b/src/framework/theme/components/chat/chat.component.ts @@ -0,0 +1,249 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { + Component, + Input, + HostBinding, + ViewChild, + ElementRef, + AfterViewChecked, + ContentChildren, + QueryList, AfterViewInit, +} from '@angular/core'; +import { NbChatMessageComponent } from './chat-message.component'; + +/** + * Conversational UI collection - a set of components for chat-like UI construction. + * + * Main features: + * - different message types support (text, image, file, file group, map, etc) + * - drag & drop for images and files with preview + * - different UI styles + * - custom action buttons (coming soon) + * + * Here's a complete example build in a bot-like app. Type `help` to be able to receive different message types. + * Enjoy the conversation and the beautiful UI. + * @stacked-example(Showcase, chat/chat-showcase.component) + * + * Basic chat configuration and usage: + * ```ts + * + * + * + * + * + * + * + * ``` + * + * There are three main components: + * ```ts + * + * // chat container + * + * + * // chat form with drag&drop files feature + * + * + * // chat message, available multiple types + * ``` + * + * Two users conversation showcase: + * @stacked-example(Conversation, chat/chat-conversation-showcase.component) + * + * Chat UI is also available in different colors by specifying a `[status]` input: + * + * @stacked-example(Colored Chat, chat/chat-colors.component) + * + * Also it is possible to configure sizes through `[size]` input: + * + * @stacked-example(Chat Sizes, chat/chat-sizes.component) + * + * @styles + * + * chat-font-size: + * chat-fg: + * chat-bg: + * chat-border-radius: + * chat-fg-text: + * chat-height-xxsmall: + * chat-height-xsmall: + * chat-height-small: + * chat-height-medium: + * chat-height-large: + * chat-height-xlarge: + * chat-height-xxlarge: + * chat-border: + * chat-padding: + * chat-shadow: + * chat-separator: + * chat-active-bg: + * chat-disabled-bg: + * chat-disabled-fg: + * chat-primary-bg: + * chat-info-bg: + * chat-success-bg: + * chat-warning-bg: + * chat-danger-bg: + */ +@Component({ + selector: 'nb-chat', + styleUrls: ['./chat.component.scss'], + template: ` +
{{ title }}
+
+
+ +

No messages yet.

+
+
+
+ +
+ `, +}) +export class NbChatComponent implements AfterViewChecked, AfterViewInit { + + static readonly SIZE_XXSMALL = 'xxsmall'; + static readonly SIZE_XSMALL = 'xsmall'; + static readonly SIZE_SMALL = 'small'; + static readonly SIZE_MEDIUM = 'medium'; + static readonly SIZE_LARGE = 'large'; + static readonly SIZE_XLARGE = 'xlarge'; + static readonly SIZE_XXLARGE = 'xxlarge'; + + static readonly STATUS_ACTIVE = 'active'; + static readonly STATUS_DISABLED = 'disabled'; + static readonly STATUS_PRIMARY = 'primary'; + static readonly STATUS_INFO = 'info'; + static readonly STATUS_SUCCESS = 'success'; + static readonly STATUS_WARNING = 'warning'; + static readonly STATUS_DANGER = 'danger'; + + size: string; + status: string; + accent: string; + + @Input() title: string; + + @HostBinding('class.xxsmall-chat') + get xxsmall() { + return this.size === NbChatComponent.SIZE_XXSMALL; + } + + @HostBinding('class.xsmall-chat') + get xsmall() { + return this.size === NbChatComponent.SIZE_XSMALL; + } + + @HostBinding('class.small-chat') + get small() { + return this.size === NbChatComponent.SIZE_SMALL; + } + + @HostBinding('class.medium-chat') + get medium() { + return this.size === NbChatComponent.SIZE_MEDIUM; + } + + @HostBinding('class.large-chat') + get large() { + return this.size === NbChatComponent.SIZE_LARGE; + } + + @HostBinding('class.xlarge-chat') + get xlarge() { + return this.size === NbChatComponent.SIZE_XLARGE; + } + + @HostBinding('class.xxlarge-chat') + get xxlarge() { + return this.size === NbChatComponent.SIZE_XXLARGE; + } + + @HostBinding('class.active-chat') + get active() { + return this.status === NbChatComponent.STATUS_ACTIVE; + } + + @HostBinding('class.disabled-chat') + get disabled() { + return this.status === NbChatComponent.STATUS_DISABLED; + } + + @HostBinding('class.primary-chat') + get primary() { + return this.status === NbChatComponent.STATUS_PRIMARY; + } + + @HostBinding('class.info-chat') + get info() { + return this.status === NbChatComponent.STATUS_INFO; + } + + @HostBinding('class.success-chat') + get success() { + return this.status === NbChatComponent.STATUS_SUCCESS; + } + + @HostBinding('class.warning-chat') + get warning() { + return this.status === NbChatComponent.STATUS_WARNING; + } + + @HostBinding('class.danger-chat') + get danger() { + return this.status === NbChatComponent.STATUS_DANGER; + } + + @HostBinding('class.accent') + get hasAccent() { + return this.accent; + } + + /** + * Chat size, available sizes: + * xxsmall, xsmall, small, medium, large, xlarge, xxlarge + * @param {string} val + */ + @Input('size') + private set setSize(val: string) { + this.size = val; + } + + /** + * Chat status color (adds specific styles): + * active, disabled, primary, info, success, warning, danger + * @param {string} val + */ + @Input('status') + private set setStatus(val: string) { + this.status = val; + } + + @ViewChild('scrollable') scrollable: ElementRef; + @ContentChildren(NbChatMessageComponent) messages: QueryList; + + ngAfterViewChecked() { + this.scrollable.nativeElement.scrollTop = this.scrollable.nativeElement.scrollHeight; + } + + ngAfterViewInit() { + this.messages.changes + .subscribe((messages) => this.messages = messages); + } +} diff --git a/src/framework/theme/components/chat/chat.module.ts b/src/framework/theme/components/chat/chat.module.ts new file mode 100644 index 0000000000..25e4d9be06 --- /dev/null +++ b/src/framework/theme/components/chat/chat.module.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ModuleWithProviders, NgModule } from '@angular/core'; + +import { NbSharedModule } from '../shared/shared.module'; + +import { NbChatComponent } from './chat.component'; +import { NbChatMessageComponent } from './chat-message.component'; +import { NbChatFormComponent } from './chat-form.component'; +import { NbChatMessageTextComponent } from './chat-message-text.component'; +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'; + +const NB_CHAT_COMPONENTS = [ + NbChatComponent, + NbChatMessageComponent, + NbChatFormComponent, + NbChatMessageTextComponent, + NbChatMessageFileComponent, + NbChatMessageQuoteComponent, + NbChatMessageMapComponent, +]; + +@NgModule({ + imports: [ + NbSharedModule, + ], + declarations: [ + ...NB_CHAT_COMPONENTS, + ], + exports: [ + ...NB_CHAT_COMPONENTS, + ], +}) +export class NbChatModule { + + static forRoot(options?: NbChatOptions) { + return { + ngModule: NbChatModule, + providers: [ + { provide: NbChatOptions, useValue: options }, + ], + }; + } + + static forChild(options?: NbChatOptions) { + return { + ngModule: NbChatModule, + providers: [ + { provide: NbChatOptions, useValue: options }, + ], + }; + } +} diff --git a/src/framework/theme/components/chat/chat.options.ts b/src/framework/theme/components/chat/chat.options.ts new file mode 100644 index 0000000000..2042cfe9dd --- /dev/null +++ b/src/framework/theme/components/chat/chat.options.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export class NbChatOptions { + messageGoogleMapKey?: string; +} diff --git a/src/framework/theme/index.ts b/src/framework/theme/index.ts index d328206cda..134850a450 100644 --- a/src/framework/theme/index.ts +++ b/src/framework/theme/index.ts @@ -35,3 +35,11 @@ export * from './components/progress-bar/progress-bar.component'; export * from './components/progress-bar/progress-bar.module'; export * from './components/alert/alert.component'; export * from './components/alert/alert.module'; +export * from './components/chat/chat.component'; +export * from './components/chat/chat-message.component'; +export * from './components/chat/chat-message-map.component'; +export * from './components/chat/chat-message-file.component'; +export * from './components/chat/chat-message-quote.component'; +export * from './components/chat/chat-message-text.component'; +export * from './components/chat/chat-form.component'; +export * from './components/chat/chat.module'; diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss index 9ecc359fe4..6cca0ac345 100644 --- a/src/framework/theme/styles/global/_components.scss +++ b/src/framework/theme/styles/global/_components.scss @@ -21,6 +21,7 @@ @import '../../components/popover/popover.component.theme'; @import '../../components/context-menu/context-menu.component.theme'; @import '../../components/alert/alert.component.theme'; +@import '../../components/chat/chat.component.theme'; @mixin nb-theme-components() { @@ -41,4 +42,5 @@ @include nb-popover-theme(); @include nb-context-menu-theme(); @include nb-alert-theme(); + @include nb-chat-theme(); } diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss index 25cd928059..84c6d0b365 100644 --- a/src/framework/theme/styles/themes/_default.scss +++ b/src/framework/theme/styles/themes/_default.scss @@ -492,6 +492,46 @@ $theme: ( alert-closable-padding: 3rem, alert-button-padding: 3rem, alert-margin: margin, + + chat-font-size: font-size, + chat-fg: color-white, + chat-bg: color-bg, + chat-border-radius: radius, + chat-fg-text: color-fg-text, + chat-height-xxsmall: 96px, + chat-height-xsmall: 216px, + chat-height-small: 336px, + chat-height-medium: 456px, + chat-height-large: 576px, + chat-height-xlarge: 696px, + chat-height-xxlarge: 816px, + chat-border: border, + chat-padding: padding, + chat-shadow: shadow, + chat-separator: separator, + chat-message-fg: color-white, + chat-message-bg: linear-gradient(to right, #4ca6ff, #59bfff), + chat-message-reply-bg: color-bg-active, + chat-message-reply-fg: color-fg-text, + chat-message-avatar-bg: color-fg, + chat-message-sender-fg: color-fg, + chat-message-quote-fg: color-fg, + chat-message-quote-bg: color-bg-active, + chat-message-file-fg: color-fg, + chat-message-file-bg: transparent, + chat-form-bg: transparent, + chat-form-fg: color-fg-heading, + chat-form-border: separator, + chat-form-placeholder-fg: color-fg, + chat-form-active-border: color-fg, + chat-active-bg: color-fg, + chat-disabled-bg: color-disabled, + chat-disabled-fg: color-fg, + chat-primary-bg: color-primary, + chat-info-bg: color-info, + chat-success-bg: color-success, + chat-warning-bg: color-warning, + chat-danger-bg: color-danger, ); // register the theme diff --git a/src/playground/chat/bot-replies.ts b/src/playground/chat/bot-replies.ts new file mode 100644 index 0000000000..23d8cd1765 --- /dev/null +++ b/src/playground/chat/bot-replies.ts @@ -0,0 +1,190 @@ +const botAvatar: string = 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg'; + +export const gifsLinks: string[] = [ + 'https://media.tenor.com/images/ac287fd06319e47b1533737662d5bfe8/tenor.gif', + 'https://i.gifer.com/no.gif', + 'https://techcrunch.com/wp-content/uploads/2015/08/safe_image.gif', + 'http://www.reactiongifs.com/r/wnd1.gif', +]; +export const imageLinks: string[] = [ + 'https://picsum.photos/320/240/?image=357', + 'https://picsum.photos/320/240/?image=556', + 'https://picsum.photos/320/240/?image=339', + 'https://picsum.photos/320/240/?image=387', + 'https://picsum.photos/320/240/?image=30', + 'https://picsum.photos/320/240/?image=271', +]; +const fileLink: string = 'http://google.com'; + +export const botReplies = [ + { + regExp: /([H,h]ey)|([H,h]i)/g, + answerArray: ['Hello!', 'Yes?', 'Yes, milord?', 'What can I do for you?'], + type: 'text', + reply: { + text: '', + reply: false, + date: new Date(), + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([H,h]elp)/g, + answerArray: [`No problem! Try sending a message containing word "hey", "image", + "gif", "file", "map", "quote", "file group" to see different message components`], + type: 'text', + reply: { + text: '', + reply: false, + date: new Date(), + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([I,i]mage)|(IMAGE)|([P,p]ic)|(Picture)/g, + answerArray: ['Hey look at this!', 'Ready to work', 'Yes, master.'], + type: 'pic', + reply: { + text: '', + reply: false, + date: new Date(), + type: 'file', + files: [ + { + url: '', + type: 'image/jpeg', + }, + ], + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([G,g]if)|(GIF)/g, + type: 'gif', + answerArray: ['No problem', 'Well done', 'You got it man'], + reply: { + text: '', + reply: false, + date: new Date(), + type: 'file', + files: [ + { + url: '', + type: 'image/gif', + }, + ], + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([F,f]ile group)|(FILE)/g, + type: 'group', + answerArray: ['Take it!', 'Job Done.', 'As you wish'], + reply: { + text: '', + reply: false, + date: new Date(), + type: 'file', + files: [ + { + url: fileLink, + icon: 'nb-compose', + }, + { + url: '', + type: 'image/gif', + }, + { + url: '', + type: 'image/jpeg', + }, + ], + icon: 'nb-compose', + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([F,f]ile)|(FILE)/g, + type: 'file', + answerArray: ['Take it!', 'Job Done.', 'As you wish'], + reply: { + text: '', + reply: false, + date: new Date(), + type: 'file', + files: [ + { + url: fileLink, + icon: 'nb-compose', + }, + ], + icon: 'nb-compose', + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([M,m]ap)|(MAP)/g, + type: 'map', + answerArray: ['Done.', 'My sight is yours.', 'I shall be your eyes.'], + reply: { + text: '', + reply: false, + date: new Date(), + type: 'map', + latitude: 53.914321, + longitude: 27.5998355, + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /([Q,q]uote)|(QUOTE)/g, + type: 'quote', + answerArray: ['Quoted!', 'Say no more.', 'I gladly obey.'], + reply: { + text: '', + reply: false, + date: new Date(), + type: 'quote', + quote: '', + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, + { + regExp: /(.*)/g, + answerArray: ['Hello there! Try typing "help"'], + type: 'text', + reply: { + text: '', + reply: false, + date: new Date(), + user: { + name: 'Bot', + avatar: botAvatar, + }, + }, + }, +]; diff --git a/src/playground/chat/chat-colors.component.html b/src/playground/chat/chat-colors.component.html new file mode 100644 index 0000000000..e87d9f8e1f --- /dev/null +++ b/src/playground/chat/chat-colors.component.html @@ -0,0 +1,12 @@ + + + + + diff --git a/src/playground/chat/chat-colors.component.ts b/src/playground/chat/chat-colors.component.ts new file mode 100644 index 0000000000..8d8f0cbdbb --- /dev/null +++ b/src/playground/chat/chat-colors.component.ts @@ -0,0 +1,138 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-colors', + templateUrl: './chat-colors.component.html', + styles: [` + ::ng-deep nb-layout-column { + justify-content: center; + display: flex; + } + nb-chat { + width: 500px; + margin: 0.5rem 0 2rem 2rem; + } + `], +}) + +export class NbChatColorsComponent { + chats: any[] = [ + { + status: 'success', + title: 'Nebular Conversational UI Success', + messages: [ + { + text: 'Success!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'danger', + title: 'Nebular Conversational UI Danger', + messages: [ + { + text: 'Danger!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'primary', + title: 'Nebular Conversational UI Primary', + messages: [ + { + text: 'Primary!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'info', + title: 'Nebular Conversational UI Info', + messages: [ + { + text: 'Info!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'warning', + title: 'Nebular Conversational UI Warning', + messages: [ + { + text: 'Warning!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'active', + title: 'Nebular Conversational UI Active', + messages: [ + { + text: 'Active!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + { + status: 'disabled', + title: 'Nebular Conversational UI Disabled', + messages: [ + { + text: 'Disabled!', + date: new Date(), + reply: false, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + }, + ]; + + sendMessage(messages, event) { + messages.push({ + text: event.message, + date: new Date(), + reply: true, + user: { + name: 'Jonh Doe', + avatar: 'https://techcrunch.com/wp-content/uploads/2015/08/safe_image.gif', + }, + }); + } +} diff --git a/src/playground/chat/chat-conversation-showcase.component.html b/src/playground/chat/chat-conversation-showcase.component.html new file mode 100644 index 0000000000..2969ca262d --- /dev/null +++ b/src/playground/chat/chat-conversation-showcase.component.html @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/src/playground/chat/chat-conversation-showcase.component.ts b/src/playground/chat/chat-conversation-showcase.component.ts new file mode 100644 index 0000000000..a5ef2ac06a --- /dev/null +++ b/src/playground/chat/chat-conversation-showcase.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-conversation-showcase', + styles: [` + ::ng-deep nb-layout-column { + display: flex; + justify-content: center; + } + :host { + display: flex; + } + nb-chat { + width: 300px; + margin: 1rem; + } + `], + templateUrl: './chat-conversation-showcase.component.html', +}) + +export class NbChatConversationShowcaseComponent { + + messages: any[] = []; + + sendMessage(event: any, userName: string, avatar: string, reply: boolean) { + const files = !event.files ? [] : event.files.map((file) => { + return { + url: file.src, + type: file.type, + icon: 'nb-compose', + }; + }); + + this.messages.push({ + text: event.message, + date: new Date(), + reply: reply, + type: files.length ? 'file' : 'text', + files: files, + user: { + name: userName, + avatar: avatar, + }, + }); + } +} diff --git a/src/playground/chat/chat-drop.component.html b/src/playground/chat/chat-drop.component.html new file mode 100644 index 0000000000..ab61dd905c --- /dev/null +++ b/src/playground/chat/chat-drop.component.html @@ -0,0 +1,12 @@ + + + + + diff --git a/src/playground/chat/chat-drop.component.ts b/src/playground/chat/chat-drop.component.ts new file mode 100644 index 0000000000..5b7b8a5a9a --- /dev/null +++ b/src/playground/chat/chat-drop.component.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-drop', + styles: [` + ::ng-deep nb-layout-column { + justify-content: center; + display: flex; + } + nb-chat { + width: 500px; + height: 80vw; + } + `], + templateUrl: './chat-drop.component.html', +}) + +export class NbChatDropComponent { + + messages: any[] = [ + { + text: 'Drag & drop a file or a group of files.', + date: new Date(), + reply: true, + user: { + name: 'Bot', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + ]; + + sendMessage(event) { + const files = !event.files ? [] : event.files.map((file) => { + return { + url: file.src, + type: file.type, + icon: 'nb-compose', + }; + }); + + this.messages.push({ + text: event.message, + date: new Date(), + files: files, + type: files.length ? 'file' : 'text', + reply: true, + user: { + name: 'Jonh Doe', + avatar: 'https://i.gifer.com/no.gif', + }, + }); + } +} diff --git a/src/playground/chat/chat-message-types-showcase.component.ts b/src/playground/chat/chat-message-types-showcase.component.ts new file mode 100644 index 0000000000..c5b37d4404 --- /dev/null +++ b/src/playground/chat/chat-message-types-showcase.component.ts @@ -0,0 +1,87 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-message-type-showcase', + styles: [` + nb-card-body { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + } + nb-chat { + width: 500px; + } + `], + template: ` + + + + + + + + + + + + + + + + + + + `, +}) + +export class NbChatMessageTypesShowcaseComponent { + date = new Date(); +} diff --git a/src/playground/chat/chat-showcase.component.html b/src/playground/chat/chat-showcase.component.html new file mode 100644 index 0000000000..d2a1a99163 --- /dev/null +++ b/src/playground/chat/chat-showcase.component.html @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/playground/chat/chat-showcase.component.ts b/src/playground/chat/chat-showcase.component.ts new file mode 100644 index 0000000000..2569f3f1aa --- /dev/null +++ b/src/playground/chat/chat-showcase.component.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; +import { NbChatShowcaseService } from './chat-showcase.service'; + +@Component({ + selector: 'nb-chat-showcase', + templateUrl: './chat-showcase.component.html', + providers: [ NbChatShowcaseService ], + styles: [` + ::ng-deep nb-layout-column { + justify-content: center; + display: flex; + } + nb-chat { + width: 500px; + } + `], +}) +export class NbChatShowcaseComponent { + + messages: any[]; + + constructor(protected chatShowcaseService: NbChatShowcaseService) { + this.messages = this.chatShowcaseService.loadMessages(); + } + + sendMessage(event: any) { + const files = !event.files ? [] : event.files.map((file) => { + return { + url: file.src, + type: file.type, + icon: 'nb-compose', + }; + }); + + this.messages.push({ + text: event.message, + date: new Date(), + reply: true, + type: files.length ? 'file' : 'text', + files: files, + user: { + name: 'Jonh Doe', + avatar: 'https://i.gifer.com/no.gif', + }, + }); + const botReply = this.chatShowcaseService.reply(event.message); + if (botReply) { + setTimeout(() => { this.messages.push(botReply) }, 500); + } + } +} diff --git a/src/playground/chat/chat-showcase.service.ts b/src/playground/chat/chat-showcase.service.ts new file mode 100644 index 0000000000..ccbc7d6d51 --- /dev/null +++ b/src/playground/chat/chat-showcase.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; + +import { messages } from './messages'; +import { botReplies, gifsLinks, imageLinks } from './bot-replies'; + +@Injectable() +export class NbChatShowcaseService { + + + loadMessages() { + return messages; + } + + loadBotReplies() { + return botReplies; + } + + reply(message: string) { + const botReply: any = this.loadBotReplies() + .find((reply: any) => message.search(reply.regExp) !== -1); + + if (botReply.reply.type === 'quote') { + botReply.reply.quote = message; + } + + if (botReply.type === 'gif') { + botReply.reply.files[0].url = gifsLinks[Math.floor(Math.random() * gifsLinks.length)]; + } + + if (botReply.type === 'pic') { + botReply.reply.files[0].url = imageLinks[Math.floor(Math.random() * imageLinks.length)]; + } + + if (botReply.type === 'group') { + botReply.reply.files[1].url = gifsLinks[Math.floor(Math.random() * gifsLinks.length)]; + botReply.reply.files[2].url = imageLinks[Math.floor(Math.random() * imageLinks.length)]; + } + + botReply.reply.text = botReply.answerArray[Math.floor(Math.random() * botReply.answerArray.length)]; + return { ...botReply.reply }; + } +} diff --git a/src/playground/chat/chat-size.component.html b/src/playground/chat/chat-size.component.html new file mode 100644 index 0000000000..e9e5db0b17 --- /dev/null +++ b/src/playground/chat/chat-size.component.html @@ -0,0 +1,12 @@ + + + + + diff --git a/src/playground/chat/chat-sizes.component.ts b/src/playground/chat/chat-sizes.component.ts new file mode 100644 index 0000000000..b9734027ba --- /dev/null +++ b/src/playground/chat/chat-sizes.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-sizes', + styles: [` + ::ng-deep nb-layout-column { + justify-content: center; + display: flex; + } + nb-chat { + width: 500px; + margin: 0.5rem 0 2rem 2rem; + }`], + templateUrl: './chat-size.component.html', +}) + +export class NbChatSizesComponent { + chats: any[] = [ + { + title: 'Nebular Conversational UI Small', + messages: [ + { + text: 'Small!', + date: new Date(), + reply: true, + user: { + name: 'Bot', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + ], + size: 'small', + }, + { + title: 'Nebular Conversational UI Medium', + messages: [ + { + text: 'Medium!', + date: new Date(), + reply: true, + user: { + name: 'Bot', + avatar: 'https://i.ytimg.com/vi/Erqi5ckVoEo/hqdefault.jpg', + }, + }, + ], + size: 'large', + }, + ]; + + sendMessage(messages, event) { + messages.push({ + text: event.message, + date: new Date(), + reply: true, + user: { + name: 'Jonh Doe', + avatar: 'https://techcrunch.com/wp-content/uploads/2015/08/safe_image.gif', + }, + }); + } +} diff --git a/src/playground/chat/chat-test.component.ts b/src/playground/chat/chat-test.component.ts new file mode 100644 index 0000000000..e57af130de --- /dev/null +++ b/src/playground/chat/chat-test.component.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-chat-test', + template: ` + + + + + + + + `, +}) +export class NbChatTestComponent { + messages = []; + sizes = ['xxsmall', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'xxlarge']; + statuses = ['primary', 'success', 'info', 'warning', 'danger', 'active', 'disabled']; + + chats: any[]; + + constructor() { + this.chats = this.prepareChats(); + } + + private prepareChats(): any[] { + const result = []; + + this.statuses.forEach(status => { + this.sizes.forEach(size => { + result.push({ + size, + status, + }); + }); + }); + + return result; + } + + sendMessage(event) { + this.messages.push({ + text: event.message, + date: new Date(), + reply: true, + user: { + name: 'Jonh Doe', + avatar: 'https://techcrunch.com/wp-content/uploads/2015/08/safe_image.gif', + }, + }); + } +} diff --git a/src/playground/chat/messages.ts b/src/playground/chat/messages.ts new file mode 100644 index 0000000000..14f9cd0888 --- /dev/null +++ b/src/playground/chat/messages.ts @@ -0,0 +1,85 @@ +export const messages = [ + { + text: 'Hello, how are you? This should be a very long message so that we can test how it fit into the screen.', + reply: false, + date: new Date(), + user: { + name: 'John Doe', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + text: 'Hello, how are you? This should be a very long message so that we can test how it fit into the screen.', + reply: true, + date: new Date(), + user: { + name: 'John Doe', + avatar: 'https://i.gifer.com/no.gif', + }, + }, + { + text: 'Hello, how are you?', + reply: false, + date: new Date(), + user: { + name: 'John Doe', + avatar: '', + }, + }, + { + text: 'Hey looks at that pic I just found!', + reply: false, + date: new Date(), + type: 'file', + files: [ + { + url: 'https://i.gifer.com/no.gif', + type: 'image/jpeg', + icon: false, + }, + ], + user: { + name: 'John Doe', + avatar: '', + }, + }, + { + text: 'What do you mean by that?', + reply: false, + date: new Date(), + type: 'quote', + quote: 'Hello, how are you? This should be a very long message so that we can test how it fit into the screen.', + user: { + name: 'John Doe', + avatar: '', + }, + }, + { + text: 'Attached is an archive I mentioned', + reply: true, + date: new Date(), + type: 'file', + files: [ + { + url: 'https://i.gifer.com/no.gif', + icon: 'nb-compose', + }, + ], + user: { + name: 'John Doe', + avatar: '', + }, + }, + { + text: 'Meet me there', + reply: false, + date: new Date(), + type: 'map', + latitude: 40.714728, + longitude: -73.998672, + user: { + name: 'John Doe', + avatar: '', + }, + }, +]; diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index 1dc741cbde..ec6b66366a 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -103,6 +103,13 @@ import { NbAlertShowcaseComponent } from './alert/alert-showcase.component'; import { NbAlertColorsComponent } from './alert/alert-colors.component'; import { NbAlertAccentsComponent } from './alert/alert-accents.component'; import { NbAlertSizesComponent } from './alert/alert-sizes.component'; +import { NbChatShowcaseComponent } from './chat/chat-showcase.component'; +import { NbChatColorsComponent } from './chat/chat-colors.component'; +import { NbChatSizesComponent } from './chat/chat-sizes.component'; +import { NbChatDropComponent } from './chat/chat-drop.component'; +import { NbChatMessageTypesShowcaseComponent } from './chat/chat-message-types-showcase.component'; +import { NbChatConversationShowcaseComponent } from './chat/chat-conversation-showcase.component'; +import { NbChatTestComponent } from './chat/chat-test.component'; export const routes: Routes = [ { @@ -396,6 +403,39 @@ export const routes: Routes = [ }, ], }, + { + path: 'chat', + children: [ + { + path: 'chat-showcase.component', + component: NbChatShowcaseComponent, + }, + { + path: 'chat-colors.component', + component: NbChatColorsComponent, + }, + { + path: 'chat-sizes.component', + component: NbChatSizesComponent, + }, + { + path: 'chat-drop.component', + component: NbChatDropComponent, + }, + { + path: 'chat-message-types-showcase.component', + component: NbChatMessageTypesShowcaseComponent, + }, + { + path: 'chat-conversation-showcase.component', + component: NbChatConversationShowcaseComponent, + }, + { + path: 'chat-test.component', + component: NbChatTestComponent, + }, + ], + }, ], }, { diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts index 36c78d47be..484ed2879d 100644 --- a/src/playground/playground.module.ts +++ b/src/playground/playground.module.ts @@ -24,6 +24,7 @@ import { NbRouteTabsetModule, NbProgressBarModule, NbAlertModule, + NbChatModule, } from '@nebular/theme'; import { NbPlaygroundRoutingModule } from './playground-routing.module'; @@ -126,6 +127,13 @@ import { NbAlertColorsComponent } from './alert/alert-colors.component'; import { NbAlertAccentsComponent } from './alert/alert-accents.component'; import { NbAlertSizesComponent } from './alert/alert-sizes.component'; import { NbAlertTestComponent } from './alert/alert-test.component'; +import { NbChatShowcaseComponent } from './chat/chat-showcase.component'; +import { NbChatColorsComponent } from './chat/chat-colors.component'; +import { NbChatSizesComponent } from './chat/chat-sizes.component'; +import { NbChatDropComponent } from './chat/chat-drop.component'; +import { NbChatMessageTypesShowcaseComponent } from './chat/chat-message-types-showcase.component'; +import { NbChatConversationShowcaseComponent } from './chat/chat-conversation-showcase.component'; +import { NbChatTestComponent } from './chat/chat-test.component'; export const NB_MODULES = [ NbCardModule, @@ -147,6 +155,9 @@ export const NB_MODULES = [ NbAlertModule, NbPlaygroundSharedModule, NbProgressBarModule, + NbChatModule.forChild({ + messageGoogleMapKey: 'AIzaSyA_wNuCzia92MAmdLRzmqitRGvCF7wCZPY', + }), ]; export const NB_EXAMPLE_COMPONENTS = [ @@ -241,6 +252,13 @@ export const NB_EXAMPLE_COMPONENTS = [ NbAlertAccentsComponent, NbAlertSizesComponent, NbAlertTestComponent, + NbChatShowcaseComponent, + NbChatColorsComponent, + NbChatSizesComponent, + NbChatDropComponent, + NbChatMessageTypesShowcaseComponent, + NbChatConversationShowcaseComponent, + NbChatTestComponent, ];