├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── .test.bats ├── .webpack.json ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── chat-message │ │ ├── chat-message.component.css │ │ ├── chat-message.component.html │ │ ├── chat-message.component.spec.ts │ │ └── chat-message.component.ts │ ├── chat-nav-bar │ │ ├── chat-nav-bar.component.css │ │ ├── chat-nav-bar.component.html │ │ ├── chat-nav-bar.component.spec.ts │ │ └── chat-nav-bar.component.ts │ ├── chat-page │ │ ├── chat-page.component.css │ │ ├── chat-page.component.html │ │ ├── chat-page.component.spec.ts │ │ └── chat-page.component.ts │ ├── chat-thread │ │ ├── chat-thread.component.css │ │ ├── chat-thread.component.html │ │ ├── chat-thread.component.spec.ts │ │ └── chat-thread.component.ts │ ├── chat-threads │ │ ├── chat-threads.component.css │ │ ├── chat-threads.component.html │ │ ├── chat-threads.component.spec.ts │ │ └── chat-threads.component.ts │ ├── chat-window │ │ ├── chat-window.component.css │ │ ├── chat-window.component.html │ │ ├── chat-window.component.spec.ts │ │ └── chat-window.component.ts │ ├── data │ │ └── chat-example-data.ts │ ├── message │ │ ├── message.model.ts │ │ ├── messages.service.spec.ts │ │ └── messages.service.ts │ ├── pipes │ │ ├── from-now.pipe.spec.ts │ │ └── from-now.pipe.ts │ ├── thread │ │ ├── thread.model.ts │ │ ├── threads.service.spec.ts │ │ └── threads.service.ts │ ├── user │ │ ├── user.model.ts │ │ └── users.service.ts │ └── util │ │ └── uuid.ts ├── assets │ ├── .gitkeep │ ├── css │ │ ├── chat.scss │ │ └── styles.scss │ ├── fonts │ │ └── bootstrap │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ └── images │ │ ├── avatars │ │ ├── female-avatar-1.png │ │ ├── female-avatar-2.png │ │ ├── female-avatar-3.png │ │ ├── female-avatar-4.png │ │ ├── male-avatar-1.png │ │ ├── male-avatar-2.png │ │ ├── male-avatar-3.png │ │ └── male-avatar-4.png │ │ ├── logos │ │ ├── Angular2RxJSChatHeaderImage.png │ │ └── ng-book-2-minibook.png │ │ └── readme │ │ ├── full-chat-preview.png │ │ ├── ng-book-2-as-book-cover-pigment.png │ │ ├── rx-chat-echo-bot.png │ │ ├── rx-chat-models.png │ │ └── rx-chat-top-level-components.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-redux-chat" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | } 52 | }, 53 | "defaults": { 54 | "styleExt": "css", 55 | "component": {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | 35 | # e2e 36 | /e2e/*.js 37 | /e2e/*.map 38 | 39 | #System Files 40 | .DS_Store 41 | Thumbs.db 42 | -------------------------------------------------------------------------------- /.test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | DIR=$(dirname $BATS_TEST_FILENAME) 3 | 4 | load "${NGBOOK_ROOT}/scripts/bats/fullstack.bats" 5 | load "${NGBOOK_ROOT}/scripts/bats-support/load.bash" 6 | load "${NGBOOK_ROOT}/scripts/bats-assert/load.bash" 7 | 8 | @test "angular-rxjs-chat unit tests pass" { 9 | cd $DIR 10 | run ng test --single-run 11 | assert_output --partial 'SUCCESS' 12 | } 13 | 14 | @test "angular-rxjs-chat e2e tests pass" { 15 | cd $DIR 16 | run_ng_e2e $TEST_TMP_DIR 17 | run cat ${TEST_TMP_DIR}/log.txt 18 | assert_output --partial 'SUCCESS' 19 | } 20 | 21 | @test "angular-rxjs-chat linting passes" { 22 | cd $DIR 23 | run npm run lint 24 | assert_output --partial 'All files pass linting' 25 | } 26 | 27 | setup() { 28 | echo "travis_fold:start:angular-rxjs-chat" 29 | cd $DIR 30 | TEST_TMP_DIR="$(mktemp -d -t fullstackXXX)" 31 | kill_ng_cli || : 32 | kill_by_port 4200 33 | true 34 | } 35 | 36 | teardown() { 37 | cd $DIR 38 | kill_ng_cli || : 39 | kill_by_port 4200 40 | echo "travis_fold:end:angular-rxjs-chat" 41 | true 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Angular 2 RxJS Chat 3 |

4 | 5 | # Angular 2 RxJS Chat [![Join the chat at https://gitter.im/ng-book/ng-book](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ng-book/ng-book?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | > An Angular 2 chat app using [Angular 2](https://angular.io/), [RxJS](https://github.com/Reactive-Extensions/RxJS), [Angular CLI](https://github.com/angular/angular-cli), [Webpack](https://webpack.github.io/), [TypeScript](http://www.typescriptlang.org/), Services, Injectables, [Karma](http://karma-runner.github.io/), Forms, and [tslint](http://palantir.github.io/tslint/) by the [ng-book 2 team](https://ng-book.com/2) 8 | 9 | This repo shows an example chat application using RxJS and Angular 2. The goal is to show how to use the Observables data architecture pattern within Angular 2. It also features: 10 | 11 | * Angular CLI, which configures Webpack with TypeScript, Karma, and tslint 12 | * Writing async components that work with RxJS 13 | * How to write injectable services in Angular 2 14 | * And much more 15 | 16 |

17 | Angular 2 RxJS Chat 18 |

19 | 20 | > Try the live [demo here](http://rxjs.ng-book.com) 21 | 22 | ## Quick start 23 | 24 | ```bash 25 | # clone the repo 26 | git clone https://github.com/ng-book/angular2-rxjs-chat.git 27 | 28 | # change into the repo directory 29 | cd angular2-rxjs-chat 30 | 31 | # install 32 | npm install 33 | 34 | # run 35 | npm start 36 | ``` 37 | 38 | Then visit [http://localhost:4200](http://localhost:4200) in your browser. 39 | 40 | ## Architecture 41 | 42 | The app has three models: 43 | 44 | * [`Message`](src/app/message/message.model.ts) - holds individual chat messages 45 | * [`Thread`](src/app/thread/thread.model.ts) - holds metadata for a group of `Message`s 46 | * [`User`](src/app/user/user.model.ts) - holds data about an individual user 47 | 48 |

49 | Model Diagram 50 |

51 | 52 | And there are three services, one for each model: 53 | 54 | * [`MessagesService`](src/app/message/messages.service.ts) - manages streams of `Message`s 55 | * [`ThreadsService`](src/app/thread/threads.service.ts) - manages streams of `Thread`s 56 | * [`UserService`](src/app/user/users.service.ts) - manages a stream of the current `User` 57 | 58 | There are also three top-level components: 59 | 60 | * [`ChatNavBar`](src/app/chat-nav-bar/chat-nav-bar.component.ts) - for the top navigation bar and unread messages count 61 | * [`ChatThreads`](src/app/chat-threads/chat-threads.component.ts) - for our clickable list of threads 62 | * [`ChatWindow`](src/app/chat-window/chat-window.component.ts) - where we hold our current conversation 63 | 64 |

65 | Angular 2 RxJS Chat 66 |

67 | 68 | ## Services Manage Observables 69 | 70 | Each service publishes data as RxJS streams. The service clients subscribe to these streams to be notified of changes. 71 | 72 | The `MessagesService` is the backbone of the application. All new messages are added to the `newMessages` stream and, more or less, all streams are derived from listening to `newMessages`. Even the `Thread`s exposed by the `ThreadsService` are created by listening to the stream of `Message`s. 73 | 74 | There are several other helpful streams that the services expose: 75 | 76 | For example, the `MessagesService` exposes the `messages` stream which is a stream of _the list of the all current messages_. That is, `messages` emits an array for each record. 77 | 78 | Similarly, the `ThreadsService` exposes a list of the chronologically-ordered threads in `orderedThreads` and so on. 79 | 80 | Understanding how RxJS streams can be tricky, but this code is heavily commented. One strategy to grokking this code is to start at the components and see how they use the services. The other strategy is to get a copy of [ng-book 2](https://ng-book.com/2) where we explain each line in detail over ~60 pages. 81 | 82 | ## Bots 83 | 84 | This app implements a few simple chat bots. For instance: 85 | 86 | * Echo bot 87 | * Reversing bot 88 | * Waiting bot 89 | 90 | Angular 2 RxJS Chat Bots 91 | 92 |
93 | 94 | ## Detailed Installation 95 | 96 | **Step 1: Install Node.js from the [Node Website](http://nodejs.org/).** 97 | 98 | We recommend Node version 4.1 or above. You can check your node version by running this: 99 | 100 | ```bash 101 | $ node -v 102 | vv4.1... 103 | ``` 104 | 105 | **Step 2: Install Dependencies** 106 | 107 | ```bash 108 | npm install 109 | ``` 110 | 111 | ## Running the App 112 | 113 | ```bash 114 | npm run go 115 | ``` 116 | 117 | Then visit [http://localhost:4200](http://localhost:4200) in your browser. 118 | 119 | ## Running the Tests 120 | 121 | You can run the unit tests with: 122 | 123 | ```bash 124 | npm run test 125 | ``` 126 | 127 | ## Future Plans 128 | 129 | There are two big changes we plan to make to this repo: 130 | 131 | ### 1. Add HTTP Requests 132 | 133 | Currently the bots are all client-side and there are no HTTP requests involved in the chats. 134 | 135 | We will move the chat bots to a server and integrate API requests into this project once the Angular 2 HTTP client development has settled down. 136 | 137 | ### 2. `ON_PUSH` change detection 138 | 139 | Because we're using observables, we can improve the performance of these components by using `ON_PUSH` change detection. Again, once Angular 2 development stabilizes, we'll be making this change. 140 | 141 | ## Contributing 142 | 143 | There are lots of other little things that need cleaned up such as: 144 | 145 | - More tests 146 | - Cleaning up the vendor scripts / typings 147 | - Simplifying the unread messages count 148 | 149 | If you'd like to contribute, feel free to submit a pull request and we'll likely merge it in. 150 | 151 | ## Getting Help 152 | 153 | If you're having trouble getting this project running, feel free to [open an issue](https://github.com/ng-book/angular2-rxjs-chat/issues), join us on [Gitter](https://gitter.im/ng-book/ng-book?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge), or [email us](mailto:us@fullstack.io)! 154 | 155 | ___ 156 | 157 | # ng-book 2 158 | 159 | 160 | ng-book 2 161 | 162 | 163 | This repo was written and is maintained by the [ng-book 2](https://ng-book.com/2) team. In the book we talk about each line of code in this app and explain why it's there and how it works. 164 | 165 | This app is only one of several apps we have in the book. If you're looking to learn Angular 2, there's no faster way than by spending a few hours with ng-book 2. 166 | 167 |
168 | 169 | ## License 170 | [MIT](/LICENSE.md) 171 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AngularChatPage } from './app.po'; 2 | import { browser } from 'protractor'; 3 | 4 | describe('angular-rxjs-chat App', () => { 5 | let page: AngularChatPage; 6 | 7 | beforeEach(() => { 8 | page = new AngularChatPage(); 9 | }); 10 | 11 | it('should load the page', () => { 12 | page.navigateTo(); 13 | 14 | expect(page.unreadCount()).toMatch(`4`); 15 | 16 | page.clickThread(1); 17 | expect(page.unreadCount()).toMatch(`3`); 18 | 19 | page.clickThread(2); 20 | expect(page.unreadCount()).toMatch(`2`); 21 | page.sendMessage('3'); 22 | 23 | page.clickThread(3); 24 | expect(page.unreadCount()).toMatch(`0`); 25 | 26 | page.clickThread(0); 27 | // expect(page.unreadCount()).toMatch(`1`); 28 | // expect(page.getConversationText(3)).toContain(`I waited 3 seconds`); 29 | 30 | browser.sleep(5000).then(function() { 31 | expect(page.getConversationText(0)).toContain(`I waited 3 seconds`); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class AngularChatPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | 12 | getHeaderText() { 13 | return element(by.css('h1')).getText(); 14 | } 15 | 16 | unreadCount() { 17 | return element(by.css('.badge')).getText(); 18 | } 19 | 20 | clickThread(i) { 21 | return element.all(by.css('chat-thread')).get(i).click(); 22 | } 23 | 24 | sendMessage(msg) { 25 | element(by.css('.chat-input')).sendKeys(msg); 26 | return element(by.buttonText('Send')).click(); 27 | } 28 | 29 | getConversationText(i) { 30 | return element.all(by.css('.conversation')).get(i).getText(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../dist/out-tsc-e2e", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "types":[ 15 | "jasmine", 16 | "node" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular/cli'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-redux-chat", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "4.2.0", 16 | "@angular/compiler": "4.2.0", 17 | "@angular/core": "4.2.0", 18 | "@angular/forms": "4.2.0", 19 | "@angular/http": "4.2.0", 20 | "@angular/platform-browser": "4.2.0", 21 | "@angular/platform-browser-dynamic": "4.2.0", 22 | "@angular/router": "4.2.0", 23 | "@types/lodash": "4.14.55", 24 | "core-js": "2.4.1", 25 | "lodash": "4.17.4", 26 | "moment": "2.18.0", 27 | "redux": "3.6.0", 28 | "reselect": "2.5.4", 29 | "rxjs": "5.0.1", 30 | "zone.js": "0.8.10", 31 | "reflect-metadata": "0.1.3", 32 | "@types/jasmine": "2.5.40" 33 | }, 34 | "devDependencies": { 35 | "@angular/cli": "1.2.0-beta.1", 36 | "@angular/compiler-cli": "4.2.0", 37 | "@types/jasmine": "2.5.38", 38 | "@types/node": "~6.0.60", 39 | "codelyzer": "~2.0.0", 40 | "jasmine-core": "~2.5.2", 41 | "jasmine-spec-reporter": "~3.2.0", 42 | "karma": "~1.4.1", 43 | "karma-chrome-launcher": "~2.0.0", 44 | "karma-cli": "~1.0.1", 45 | "karma-jasmine": "~1.1.0", 46 | "karma-jasmine-html-reporter": "0.2.2", 47 | "karma-coverage-istanbul-reporter": "0.2.0", 48 | "protractor": "~5.1.0", 49 | "ts-node": "~2.0.0", 50 | "tslint": "~4.4.2", 51 | "typescript": "2.3.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | xdescribe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app works!'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app works!'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { ChatExampleData } from './data/chat-example-data'; 3 | 4 | import { UsersService } from './user/users.service'; 5 | import { ThreadsService } from './thread/threads.service'; 6 | import { MessagesService } from './message/messages.service'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.css'] 12 | }) 13 | export class AppComponent { 14 | constructor(public messagesService: MessagesService, 15 | public threadsService: ThreadsService, 16 | public usersService: UsersService) { 17 | ChatExampleData.init(messagesService, threadsService, usersService); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | 6 | import { UsersService } from './user/users.service'; 7 | import { ThreadsService } from './thread/threads.service'; 8 | import { MessagesService } from './message/messages.service'; 9 | 10 | import { AppComponent } from './app.component'; 11 | import { ChatMessageComponent } from './chat-message/chat-message.component'; 12 | import { ChatThreadComponent } from './chat-thread/chat-thread.component'; 13 | import { ChatNavBarComponent } from './chat-nav-bar/chat-nav-bar.component'; 14 | import { ChatThreadsComponent } from './chat-threads/chat-threads.component'; 15 | import { ChatWindowComponent } from './chat-window/chat-window.component'; 16 | import { ChatPageComponent } from './chat-page/chat-page.component'; 17 | import { FromNowPipe } from './pipes/from-now.pipe'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | ChatMessageComponent, 23 | ChatThreadComponent, 24 | ChatNavBarComponent, 25 | ChatThreadsComponent, 26 | ChatWindowComponent, 27 | ChatPageComponent, 28 | FromNowPipe 29 | ], 30 | imports: [ 31 | BrowserModule, 32 | FormsModule, 33 | HttpModule 34 | ], 35 | providers: [ 36 | MessagesService, ThreadsService, UsersService 37 | ], 38 | 39 | bootstrap: [AppComponent] 40 | }) 41 | export class AppModule { } 42 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-message/chat-message.component.css -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
6 | 7 |
8 | 9 |
11 |

{{message.text}}

12 |

{{message.sender}} • {{message.sentAt | fromNow}}

13 |
14 | 15 |
17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessageComponent } from './chat-message.component'; 4 | 5 | xdescribe('ChatMessageComponent', () => { 6 | let component: ChatMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatMessageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input 5 | } from '@angular/core'; 6 | import { Observable } from 'rxjs'; 7 | 8 | import { UsersService } from './../user/users.service'; 9 | import { ThreadsService } from './../thread/threads.service'; 10 | import { MessagesService } from './../message/messages.service'; 11 | 12 | import { Message } from './../message/message.model'; 13 | import { Thread } from './../thread/thread.model'; 14 | import { User } from './../user/user.model'; 15 | 16 | @Component({ 17 | selector: 'chat-message', 18 | templateUrl: './chat-message.component.html', 19 | styleUrls: ['./chat-message.component.css'] 20 | }) 21 | export class ChatMessageComponent implements OnInit { 22 | @Input() message: Message; 23 | currentUser: User; 24 | incoming: boolean; 25 | 26 | constructor(public UsersService: UsersService) { 27 | } 28 | 29 | ngOnInit(): void { 30 | this.UsersService.currentUser 31 | .subscribe( 32 | (user: User) => { 33 | this.currentUser = user; 34 | if (this.message.author && user) { 35 | this.incoming = this.message.author.id !== user.id; 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-nav-bar/chat-nav-bar.component.css -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatNavBarComponent } from './chat-nav-bar.component'; 4 | 5 | xdescribe('ChatNavBarComponent', () => { 6 | let component: ChatNavBarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatNavBarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatNavBarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-nav-bar/chat-nav-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Inject, 4 | OnInit 5 | } from '@angular/core'; 6 | import * as _ from 'lodash'; 7 | 8 | import { ThreadsService } from './../thread/threads.service'; 9 | import { MessagesService } from './../message/messages.service'; 10 | 11 | import { Thread } from './../thread/thread.model'; 12 | import { Message } from './../message/message.model'; 13 | 14 | @Component({ 15 | selector: 'chat-nav-bar', 16 | templateUrl: './chat-nav-bar.component.html', 17 | styleUrls: ['./chat-nav-bar.component.css'] 18 | }) 19 | export class ChatNavBarComponent implements OnInit { 20 | unreadMessagesCount: number; 21 | 22 | constructor(public messagesService: MessagesService, 23 | public threadsService: ThreadsService) { 24 | } 25 | 26 | ngOnInit(): void { 27 | this.messagesService.messages 28 | .combineLatest( 29 | this.threadsService.currentThread, 30 | (messages: Message[], currentThread: Thread) => 31 | [currentThread, messages] ) 32 | 33 | .subscribe(([currentThread, messages]: [Thread, Message[]]) => { 34 | this.unreadMessagesCount = 35 | _.reduce( 36 | messages, 37 | (sum: number, m: Message) => { 38 | const messageIsInCurrentThread: boolean = m.thread && 39 | currentThread && 40 | (currentThread.id === m.thread.id); 41 | // note: in a "real" app you should also exclude 42 | // messages that were authored by the current user b/c they've 43 | // already been "read" 44 | if (m && !m.isRead && !messageIsInCurrentThread) { 45 | sum = sum + 1; 46 | } 47 | return sum; 48 | }, 49 | 0); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-page/chat-page.component.css -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatPageComponent } from './chat-page.component'; 4 | 5 | xdescribe('ChatPageComponent', () => { 6 | let component: ChatPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-page/chat-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'chat-page', 5 | templateUrl: './chat-page.component.html', 6 | styleUrls: ['./chat-page.component.css'] 7 | }) 8 | export class ChatPageComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-thread/chat-thread.component.css -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |
6 |
7 |
{{thread.name}} 8 | 9 |
10 | {{thread.lastMessage.text}} 11 |
12 | Select 13 |
14 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatThreadComponent } from './chat-thread.component'; 4 | 5 | xdescribe('ChatThreadComponent', () => { 6 | let component: ChatThreadComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatThreadComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatThreadComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-thread/chat-thread.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Input, 5 | Output, 6 | EventEmitter 7 | } from '@angular/core'; 8 | import { Observable } from 'rxjs'; 9 | import { ThreadsService } from './../thread/threads.service'; 10 | import { Thread } from '../thread/thread.model'; 11 | 12 | @Component({ 13 | selector: 'chat-thread', 14 | templateUrl: './chat-thread.component.html', 15 | styleUrls: ['./chat-thread.component.css'] 16 | }) 17 | export class ChatThreadComponent implements OnInit { 18 | @Input() thread: Thread; 19 | selected = false; 20 | 21 | constructor(public threadsService: ThreadsService) { 22 | } 23 | 24 | ngOnInit(): void { 25 | this.threadsService.currentThread 26 | .subscribe( (currentThread: Thread) => { 27 | this.selected = currentThread && 28 | this.thread && 29 | (currentThread.id === this.thread.id); 30 | }); 31 | } 32 | 33 | clicked(event: any): void { 34 | this.threadsService.setCurrentThread(this.thread); 35 | event.preventDefault(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-threads/chat-threads.component.css -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 8 | 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatThreadsComponent } from './chat-threads.component'; 4 | 5 | xdescribe('ChatThreadsComponent', () => { 6 | let component: ChatThreadsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatThreadsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatThreadsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-threads/chat-threads.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | Inject 5 | } from '@angular/core'; 6 | import { Observable } from 'rxjs'; 7 | import { Thread } from '../thread/thread.model'; 8 | import { ThreadsService } from './../thread/threads.service'; 9 | 10 | @Component({ 11 | selector: 'chat-threads', 12 | templateUrl: './chat-threads.component.html', 13 | styleUrls: ['./chat-threads.component.css'] 14 | }) 15 | export class ChatThreadsComponent { 16 | threads: Observable; 17 | 18 | constructor(public threadsService: ThreadsService) { 19 | this.threads = threadsService.orderedThreads; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/app/chat-window/chat-window.component.css -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |

9 | 10 | Chat - {{currentThread.name}} 11 |

12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 | 22 | 23 |
24 | 25 | 39 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatWindowComponent } from './chat-window.component'; 4 | 5 | xdescribe('ChatWindowComponent', () => { 6 | let component: ChatWindowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatWindowComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatWindowComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat-window/chat-window.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Inject, 4 | ElementRef, 5 | OnInit, 6 | ChangeDetectionStrategy 7 | } from '@angular/core'; 8 | import { Observable } from 'rxjs'; 9 | 10 | import { User } from '../user/user.model'; 11 | import { UsersService } from '../user/users.service'; 12 | import { Thread } from '../thread/thread.model'; 13 | import { ThreadsService } from '../thread/threads.service'; 14 | import { Message } from '../message/message.model'; 15 | import { MessagesService } from '../message/messages.service'; 16 | 17 | @Component({ 18 | selector: 'chat-window', 19 | templateUrl: './chat-window.component.html', 20 | styleUrls: ['./chat-window.component.css'], 21 | changeDetection: ChangeDetectionStrategy.OnPush 22 | }) 23 | export class ChatWindowComponent implements OnInit { 24 | messages: Observable; 25 | currentThread: Thread; 26 | draftMessage: Message; 27 | currentUser: User; 28 | 29 | constructor(public messagesService: MessagesService, 30 | public threadsService: ThreadsService, 31 | public UsersService: UsersService, 32 | public el: ElementRef) { 33 | } 34 | 35 | ngOnInit(): void { 36 | this.messages = this.threadsService.currentThreadMessages; 37 | 38 | this.draftMessage = new Message(); 39 | 40 | this.threadsService.currentThread.subscribe( 41 | (thread: Thread) => { 42 | this.currentThread = thread; 43 | }); 44 | 45 | this.UsersService.currentUser 46 | .subscribe( 47 | (user: User) => { 48 | this.currentUser = user; 49 | }); 50 | 51 | this.messages 52 | .subscribe( 53 | (messages: Array) => { 54 | setTimeout(() => { 55 | this.scrollToBottom(); 56 | }); 57 | }); 58 | } 59 | 60 | onEnter(event: any): void { 61 | this.sendMessage(); 62 | event.preventDefault(); 63 | } 64 | 65 | sendMessage(): void { 66 | const m: Message = this.draftMessage; 67 | m.author = this.currentUser; 68 | m.thread = this.currentThread; 69 | m.isRead = true; 70 | this.messagesService.addMessage(m); 71 | this.draftMessage = new Message(); 72 | } 73 | 74 | scrollToBottom(): void { 75 | const scrollPane: any = this.el 76 | .nativeElement.querySelector('.msg-container-base'); 77 | scrollPane.scrollTop = scrollPane.scrollHeight; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/data/chat-example-data.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:max-line-length */ 2 | import { User } from '../user/user.model'; 3 | import { Thread } from '../thread/thread.model'; 4 | import { Message } from '../message/message.model'; 5 | import { MessagesService } from '../message/messages.service'; 6 | import { ThreadsService } from '../thread/threads.service'; 7 | import { UsersService } from '../user/users.service'; 8 | import * as moment from 'moment'; 9 | 10 | // the person using the app us Juliet 11 | const me: User = new User('Juliet', 'assets/images/avatars/female-avatar-1.png'); 12 | const ladycap: User = new User('Lady Capulet', 'assets/images/avatars/female-avatar-2.png'); 13 | const echo: User = new User('Echo Bot', 'assets/images/avatars/male-avatar-1.png'); 14 | const rev: User = new User('Reverse Bot', 'assets/images/avatars/female-avatar-4.png'); 15 | const wait: User = new User('Waiting Bot', 'assets/images/avatars/male-avatar-2.png'); 16 | 17 | const tLadycap: Thread = new Thread('tLadycap', ladycap.name, ladycap.avatarSrc); 18 | const tEcho: Thread = new Thread('tEcho', echo.name, echo.avatarSrc); 19 | const tRev: Thread = new Thread('tRev', rev.name, rev.avatarSrc); 20 | const tWait: Thread = new Thread('tWait', wait.name, wait.avatarSrc); 21 | 22 | const initialMessages: Array = [ 23 | new Message({ 24 | author: me, 25 | sentAt: moment().subtract(45, 'minutes').toDate(), 26 | text: 'Yet let me weep for such a feeling loss.', 27 | thread: tLadycap 28 | }), 29 | new Message({ 30 | author: ladycap, 31 | sentAt: moment().subtract(20, 'minutes').toDate(), 32 | text: 'So shall you feel the loss, but not the friend which you weep for.', 33 | thread: tLadycap 34 | }), 35 | new Message({ 36 | author: echo, 37 | sentAt: moment().subtract(1, 'minutes').toDate(), 38 | text: `I\'ll echo whatever you send me`, 39 | thread: tEcho 40 | }), 41 | new Message({ 42 | author: rev, 43 | sentAt: moment().subtract(3, 'minutes').toDate(), 44 | text: `I\'ll reverse whatever you send me`, 45 | thread: tRev 46 | }), 47 | new Message({ 48 | author: wait, 49 | sentAt: moment().subtract(4, 'minutes').toDate(), 50 | text: `I\'ll wait however many seconds you send to me before responding. Try sending '3'`, 51 | thread: tWait 52 | }), 53 | ]; 54 | 55 | export class ChatExampleData { 56 | static init(messagesService: MessagesService, 57 | threadsService: ThreadsService, 58 | UsersService: UsersService): void { 59 | 60 | // TODO make `messages` hot 61 | messagesService.messages.subscribe(() => ({})); 62 | 63 | // set "Juliet" as the current user 64 | UsersService.setCurrentUser(me); 65 | 66 | // create the initial messages 67 | initialMessages.map( (message: Message) => messagesService.addMessage(message) ); 68 | 69 | threadsService.setCurrentThread(tEcho); 70 | 71 | this.setupBots(messagesService); 72 | } 73 | 74 | static setupBots(messagesService: MessagesService): void { 75 | 76 | // echo bot 77 | messagesService.messagesForThreadUser(tEcho, echo) 78 | .forEach( (message: Message): void => { 79 | messagesService.addMessage( 80 | new Message({ 81 | author: echo, 82 | text: message.text, 83 | thread: tEcho 84 | }) 85 | ); 86 | }, 87 | null); 88 | 89 | 90 | // reverse bot 91 | messagesService.messagesForThreadUser(tRev, rev) 92 | .forEach( (message: Message): void => { 93 | messagesService.addMessage( 94 | new Message({ 95 | author: rev, 96 | text: message.text.split('').reverse().join(''), 97 | thread: tRev 98 | }) 99 | ); 100 | }, 101 | null); 102 | 103 | // waiting bot 104 | messagesService.messagesForThreadUser(tWait, wait) 105 | .forEach( (message: Message): void => { 106 | 107 | let waitTime: number = parseInt(message.text, 10); 108 | let reply: string; 109 | 110 | if (isNaN(waitTime)) { 111 | waitTime = 0; 112 | reply = `I didn\'t understand ${message.text}. Try sending me a number`; 113 | } else { 114 | reply = `I waited ${waitTime} seconds to send you this.`; 115 | } 116 | 117 | setTimeout( 118 | () => { 119 | messagesService.addMessage( 120 | new Message({ 121 | author: wait, 122 | text: reply, 123 | thread: tWait 124 | }) 125 | ); 126 | }, 127 | waitTime * 1000); 128 | }, 129 | null); 130 | 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/app/message/message.model.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../user/user.model'; 2 | import { Thread } from '../thread/thread.model'; 3 | import { uuid } from './../util/uuid'; 4 | 5 | /** 6 | * Message represents one message being sent in a Thread 7 | */ 8 | export class Message { 9 | id: string; 10 | sentAt: Date; 11 | isRead: boolean; 12 | author: User; 13 | text: string; 14 | thread: Thread; 15 | 16 | constructor(obj?: any) { 17 | this.id = obj && obj.id || uuid(); 18 | this.isRead = obj && obj.isRead || false; 19 | this.sentAt = obj && obj.sentAt || new Date(); 20 | this.author = obj && obj.author || null; 21 | this.text = obj && obj.text || null; 22 | this.thread = obj && obj.thread || null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/message/messages.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MessagesService } from './messages.service'; 2 | 3 | import { Message } from './message.model'; 4 | import { Thread } from './../thread/thread.model'; 5 | import { User } from './../user/user.model'; 6 | 7 | describe('MessagesService', () => { 8 | it('should test', () => { 9 | 10 | const user: User = new User('Nate', ''); 11 | const thread: Thread = new Thread('t1', 'Nate', ''); 12 | const m1: Message = new Message({ 13 | author: user, 14 | text: 'Hi!', 15 | thread: thread 16 | }); 17 | 18 | const m2: Message = new Message({ 19 | author: user, 20 | text: 'Bye!', 21 | thread: thread 22 | }); 23 | 24 | const messagesService: MessagesService = new MessagesService(); 25 | 26 | // listen to each message indivdually as it comes in 27 | messagesService.newMessages 28 | .subscribe( (message: Message) => { 29 | console.log('=> newMessages: ' + message.text); 30 | }); 31 | 32 | // listen to the stream of most current messages 33 | messagesService.messages 34 | .subscribe( (messages: Message[]) => { 35 | console.log('=> messages: ' + messages.length); 36 | }); 37 | 38 | messagesService.addMessage(m1); 39 | messagesService.addMessage(m2); 40 | 41 | // => messages: 1 42 | // => newMessages: Hi! 43 | // => messages: 2 44 | // => newMessages: Bye! 45 | }); 46 | 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/message/messages.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, Observable } from 'rxjs'; 3 | import { User } from '../user/user.model'; 4 | import { Thread } from '../thread/thread.model'; 5 | import { Message } from '../message/message.model'; 6 | 7 | const initialMessages: Message[] = []; 8 | 9 | interface IMessagesOperation extends Function { 10 | (messages: Message[]): Message[]; 11 | } 12 | 13 | @Injectable() 14 | export class MessagesService { 15 | // a stream that publishes new messages only once 16 | newMessages: Subject = new Subject(); 17 | 18 | // `messages` is a stream that emits an array of the most up to date messages 19 | messages: Observable; 20 | 21 | // `updates` receives _operations_ to be applied to our `messages` 22 | // it's a way we can perform changes on *all* messages (that are currently 23 | // stored in `messages`) 24 | updates: Subject = new Subject(); 25 | 26 | // action streams 27 | create: Subject = new Subject(); 28 | markThreadAsRead: Subject = new Subject(); 29 | 30 | constructor() { 31 | this.messages = this.updates 32 | // watch the updates and accumulate operations on the messages 33 | .scan((messages: Message[], 34 | operation: IMessagesOperation) => { 35 | return operation(messages); 36 | }, 37 | initialMessages) 38 | // make sure we can share the most recent list of messages across anyone 39 | // who's interested in subscribing and cache the last known list of 40 | // messages 41 | .publishReplay(1) 42 | .refCount(); 43 | 44 | // `create` takes a Message and then puts an operation (the inner function) 45 | // on the `updates` stream to add the Message to the list of messages. 46 | // 47 | // That is, for each item that gets added to `create` (by using `next`) 48 | // this stream emits a concat operation function. 49 | // 50 | // Next we subscribe `this.updates` to listen to this stream, which means 51 | // that it will receive each operation that is created 52 | // 53 | // Note that it would be perfectly acceptable to simply modify the 54 | // "addMessage" function below to simply add the inner operation function to 55 | // the update stream directly and get rid of this extra action stream 56 | // entirely. The pros are that it is potentially clearer. The cons are that 57 | // the stream is no longer composable. 58 | this.create 59 | .map( function(message: Message): IMessagesOperation { 60 | return (messages: Message[]) => { 61 | return messages.concat(message); 62 | }; 63 | }) 64 | .subscribe(this.updates); 65 | 66 | this.newMessages 67 | .subscribe(this.create); 68 | 69 | // similarly, `markThreadAsRead` takes a Thread and then puts an operation 70 | // on the `updates` stream to mark the Messages as read 71 | this.markThreadAsRead 72 | .map( (thread: Thread) => { 73 | return (messages: Message[]) => { 74 | return messages.map( (message: Message) => { 75 | // note that we're manipulating `message` directly here. Mutability 76 | // can be confusing and there are lots of reasons why you might want 77 | // to, say, copy the Message object or some other 'immutable' here 78 | if (message.thread.id === thread.id) { 79 | message.isRead = true; 80 | } 81 | return message; 82 | }); 83 | }; 84 | }) 85 | .subscribe(this.updates); 86 | 87 | } 88 | 89 | // an imperative function call to this action stream 90 | addMessage(message: Message): void { 91 | this.newMessages.next(message); 92 | } 93 | 94 | messagesForThreadUser(thread: Thread, user: User): Observable { 95 | return this.newMessages 96 | .filter((message: Message) => { 97 | // belongs to this thread 98 | return (message.thread.id === thread.id) && 99 | // and isn't authored by this user 100 | (message.author.id !== user.id); 101 | }); 102 | } 103 | } 104 | 105 | export const messagesServiceInjectables: Array = [ 106 | MessagesService 107 | ]; 108 | -------------------------------------------------------------------------------- /src/app/pipes/from-now.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FromNowPipe } from './from-now.pipe'; 2 | 3 | xdescribe('FromNowPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FromNowPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/pipes/from-now.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as moment from 'moment'; 3 | 4 | /** 5 | * FromNowPipe let's us convert a date into a human-readable relative-time 6 | * such as "10 minutes ago". 7 | */ 8 | @Pipe({ 9 | name: 'fromNow' 10 | }) 11 | export class FromNowPipe implements PipeTransform { 12 | transform(value: any, args: Array): string { 13 | return moment(value).fromNow(); 14 | } 15 | } 16 | 17 | export const fromNowPipeInjectables: Array = [ 18 | FromNowPipe 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/thread/thread.model.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../message/message.model'; 2 | import { uuid } from '../util/uuid'; 3 | 4 | /** 5 | * Thread represents a group of Users exchanging Messages 6 | */ 7 | export class Thread { 8 | id: string; 9 | lastMessage: Message; 10 | name: string; 11 | avatarSrc: string; 12 | 13 | constructor(id?: string, 14 | name?: string, 15 | avatarSrc?: string) { 16 | this.id = id || uuid(); 17 | this.name = name; 18 | this.avatarSrc = avatarSrc; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/thread/threads.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './../message/message.model'; 2 | import { Thread } from './thread.model'; 3 | import { User } from './../user/user.model'; 4 | 5 | import { ThreadsService } from './threads.service'; 6 | import { MessagesService } from './../message/messages.service'; 7 | import * as _ from 'lodash'; 8 | 9 | describe('ThreadsService', () => { 10 | it('should collect the Threads from Messages', () => { 11 | 12 | const nate: User = new User('Nate Murray', ''); 13 | const felipe: User = new User('Felipe Coury', ''); 14 | 15 | const t1: Thread = new Thread('t1', 'Thread 1', ''); 16 | const t2: Thread = new Thread('t2', 'Thread 2', ''); 17 | 18 | const m1: Message = new Message({ 19 | author: nate, 20 | text: 'Hi!', 21 | thread: t1 22 | }); 23 | 24 | const m2: Message = new Message({ 25 | author: felipe, 26 | text: 'Where did you get that hat?', 27 | thread: t1 28 | }); 29 | 30 | const m3: Message = new Message({ 31 | author: nate, 32 | text: 'Did you bring the briefcase?', 33 | thread: t2 34 | }); 35 | 36 | const messagesService: MessagesService = new MessagesService(); 37 | const threadsService: ThreadsService = new ThreadsService(messagesService); 38 | 39 | threadsService.threads 40 | .subscribe( (threadIdx: { [key: string]: Thread }) => { 41 | const threads: Thread[] = _.values(threadIdx); 42 | const threadNames: string = _.map(threads, (t: Thread) => t.name) 43 | .join(', '); 44 | console.log(`=> threads (${threads.length}): ${threadNames} `); 45 | }); 46 | 47 | messagesService.addMessage(m1); 48 | messagesService.addMessage(m2); 49 | messagesService.addMessage(m3); 50 | 51 | // => threads (1): Thread 1 52 | // => threads (1): Thread 1 53 | // => threads (2): Thread 1, Thread 2 54 | 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/thread/threads.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, BehaviorSubject, Observable } from 'rxjs'; 3 | import { Thread } from './thread.model'; 4 | import { Message } from '../message/message.model'; 5 | import { MessagesService } from '../message/messages.service'; 6 | import * as _ from 'lodash'; 7 | 8 | @Injectable() 9 | export class ThreadsService { 10 | 11 | // `threads` is a observable that contains the most up to date list of threads 12 | threads: Observable<{ [key: string]: Thread }>; 13 | 14 | // `orderedThreads` contains a newest-first chronological list of threads 15 | orderedThreads: Observable; 16 | 17 | // `currentThread` contains the currently selected thread 18 | currentThread: Subject = 19 | new BehaviorSubject(new Thread()); 20 | 21 | // `currentThreadMessages` contains the set of messages for the currently 22 | // selected thread 23 | currentThreadMessages: Observable; 24 | 25 | constructor(public messagesService: MessagesService) { 26 | 27 | this.threads = messagesService.messages 28 | .map( (messages: Message[]) => { 29 | const threads: {[key: string]: Thread} = {}; 30 | // Store the message's thread in our accumulator `threads` 31 | messages.map((message: Message) => { 32 | threads[message.thread.id] = threads[message.thread.id] || 33 | message.thread; 34 | 35 | // Cache the most recent message for each thread 36 | const messagesThread: Thread = threads[message.thread.id]; 37 | if (!messagesThread.lastMessage || 38 | messagesThread.lastMessage.sentAt < message.sentAt) { 39 | messagesThread.lastMessage = message; 40 | } 41 | }); 42 | return threads; 43 | }); 44 | 45 | this.orderedThreads = this.threads 46 | .map((threadGroups: { [key: string]: Thread }) => { 47 | const threads: Thread[] = _.values(threadGroups); 48 | return _.sortBy(threads, (t: Thread) => t.lastMessage.sentAt).reverse(); 49 | }); 50 | 51 | this.currentThreadMessages = this.currentThread 52 | .combineLatest(messagesService.messages, 53 | (currentThread: Thread, messages: Message[]) => { 54 | if (currentThread && messages.length > 0) { 55 | return _.chain(messages) 56 | .filter((message: Message) => 57 | (message.thread.id === currentThread.id)) 58 | .map((message: Message) => { 59 | message.isRead = true; 60 | return message; }) 61 | .value(); 62 | } else { 63 | return []; 64 | } 65 | }); 66 | 67 | this.currentThread.subscribe(this.messagesService.markThreadAsRead); 68 | } 69 | 70 | setCurrentThread(newThread: Thread): void { 71 | this.currentThread.next(newThread); 72 | } 73 | 74 | } 75 | 76 | export const threadsServiceInjectables: Array = [ 77 | ThreadsService 78 | ]; 79 | -------------------------------------------------------------------------------- /src/app/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from '../util/uuid'; 2 | 3 | /** 4 | * A User represents an agent that sends messages 5 | */ 6 | export class User { 7 | id: string; 8 | 9 | constructor(public name: string, 10 | public avatarSrc: string) { 11 | this.id = uuid(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/user/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, BehaviorSubject } from 'rxjs'; 3 | import { User } from './user.model'; 4 | 5 | 6 | /** 7 | * UserService manages our current user 8 | */ 9 | @Injectable() 10 | export class UsersService { 11 | // `currentUser` contains the current user 12 | currentUser: Subject = new BehaviorSubject(null); 13 | 14 | public setCurrentUser(newUser: User): void { 15 | this.currentUser.next(newUser); 16 | } 17 | } 18 | 19 | export const userServiceInjectables: Array = [ 20 | UsersService 21 | ]; 22 | -------------------------------------------------------------------------------- /src/app/util/uuid.ts: -------------------------------------------------------------------------------- 1 | /* jshint bitwise:false, node:true */ 2 | /* tslint:disable:no-bitwise no-var-keyword typedef */ 3 | 4 | // taken from TodoMVC 5 | export function uuid() { 6 | var i, random; 7 | var result = ''; 8 | 9 | for (i = 0; i < 32; i++) { 10 | random = Math.random() * 16 | 0; 11 | if (i === 8 || i === 12 || i === 16 || i === 20) { 12 | result += '-'; 13 | } 14 | result += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) 15 | .toString(16); 16 | } 17 | 18 | return result; 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/css/chat.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Google Hangouts-like bootstrap css originally from: 3 | http://bootsnipp.com/snippets/featured/like-hangout-chat 4 | by SrPatinhas 5 | 6 | as well as some css from "Chat": http://bootsnipp.com/snippets/featured/chat 7 | by hardiksondagar 8 | */ 9 | 10 | .conversation-wrap { 11 | box-shadow: -2px 0 3px #ddd; 12 | padding:0; 13 | @extend .col-lg-3; 14 | 15 | .conversation { 16 | position: relative; 17 | display: block; 18 | padding:5px; 19 | border-bottom:1px solid #ddd; 20 | margin:0; 21 | 22 | .avatar { 23 | width: 50px; 24 | height: 50px; 25 | } 26 | 27 | 28 | &:hover { 29 | cursor: hand; 30 | cursor: pointer; 31 | opacity: .8; 32 | } 33 | 34 | // http://ctrlq.org/code/19639-turn-div-clickable-link 35 | .div-link { 36 | position: absolute; 37 | width: 100%; 38 | height: 100%; 39 | top: 0; 40 | left: 0; 41 | text-decoration: none; 42 | /* Makes sure the link doesn't get underlined */ 43 | z-index: 10; 44 | /* raises anchor tag above everything else in div */ 45 | background-color: white; 46 | /*workaround to make clickable in IE */ 47 | opacity: 0; 48 | /*workaround to make clickable in IE */ 49 | filter: alpha(opacity=0); 50 | /*workaround to make clickable in IE */ 51 | } 52 | 53 | } 54 | 55 | &::-webkit-scrollbar { 56 | width: 6px; 57 | } 58 | 59 | &::-webkit-scrollbar-track { 60 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 61 | } 62 | 63 | &::-webkit-scrollbar-thumb { 64 | background:#ddd; 65 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); 66 | } 67 | &::-webkit-scrollbar-thumb:window-inactive { 68 | background: #ddd; 69 | } 70 | } 71 | 72 | .chat-window-container { 73 | @extend .row; 74 | } 75 | 76 | .chat-window { 77 | @extend .col-xs-10, .col-sm-5; 78 | 79 | bottom: 0; 80 | right: 0; 81 | position: fixed; 82 | float: right; 83 | margin-left: 10px; 84 | 85 | .panel-container { 86 | @extend .col-xs-12; 87 | @extend .col-md-12; 88 | } 89 | 90 | > div > .panel { 91 | border-radius: 5px 5px 0 0; 92 | } 93 | 94 | .panel-footer { 95 | .chat-input { 96 | @extend .input-sm; 97 | @extend .form-control; 98 | } 99 | 100 | .btn-chat { 101 | @extend .btn; 102 | @extend .btn-primary; 103 | @extend .btn-sm; 104 | } 105 | } 106 | 107 | .col-md-2, .col-md-10 { 108 | padding: 0; 109 | } 110 | 111 | .panel { 112 | margin-bottom: 0px; 113 | } 114 | 115 | .icon_minim { 116 | padding: 2px 10px; 117 | } 118 | 119 | .msg-container-base { 120 | background: #e5e5e5; 121 | margin: 0; 122 | padding: 0 10px 10px; 123 | max-height: 300px; 124 | overflow-x: hidden; 125 | } 126 | 127 | .top-bar { 128 | background: #666; 129 | color: white; 130 | padding: 10px; 131 | position: relative; 132 | overflow: hidden; 133 | 134 | .panel-title-container { 135 | @extend .col-md-10; 136 | @extend .col-xs-10; 137 | } 138 | .panel-buttons-container { 139 | @extend .col-md-2; 140 | @extend .col-xs-2; 141 | } 142 | } 143 | 144 | .msg-receive { 145 | padding-left: 0; 146 | margin-left: 0; 147 | } 148 | 149 | .msg-sent { 150 | padding-bottom: 20px !important; 151 | margin-right: 0; 152 | } 153 | 154 | .messages { 155 | @extend .col-md-10; 156 | @extend .col-xs-10; 157 | 158 | background: white; 159 | padding: 10px; 160 | border-radius: 2px; 161 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 162 | max-width: 100%; 163 | > { 164 | p { 165 | font-size: 13px; 166 | margin: 0 0 0.2rem 0; 167 | } 168 | .time { 169 | font-size: 11px; 170 | color: #ccc; 171 | } 172 | } 173 | } 174 | 175 | .msg-container { 176 | @extend .row; 177 | padding: 10px; 178 | overflow: hidden; 179 | display: flex; 180 | 181 | .avatar { 182 | @extend .col-md-2; 183 | @extend .col-xs-2; 184 | 185 | img { 186 | @extend .img-responsive; 187 | } 188 | } 189 | } 190 | 191 | img { 192 | display: block; 193 | width: 100%; 194 | } 195 | 196 | .avatar { 197 | position: relative; 198 | } 199 | 200 | .base-receive > .avatar:after { 201 | content: ""; 202 | position: absolute; 203 | top: 0; 204 | right: 0; 205 | width: 0; 206 | height: 0; 207 | border: 5px solid #FFF; 208 | border-left-color: rgba(0, 0, 0, 0); 209 | border-bottom-color: rgba(0, 0, 0, 0); 210 | } 211 | 212 | .base-sent { 213 | justify-content: flex-end; 214 | align-items: flex-end; 215 | > .avatar:after { 216 | content: ""; 217 | position: absolute; 218 | bottom: 0; 219 | left: 0; 220 | width: 0; 221 | height: 0; 222 | border: 5px solid white; 223 | border-right-color: transparent; 224 | border-top-color: transparent; 225 | box-shadow: 1px 1px 2px rgba(black, 0.2); 226 | } 227 | } 228 | 229 | .msg-sent > .time { 230 | float: right; 231 | } 232 | 233 | .msg-container-base { 234 | &::-webkit-scrollbar-track { 235 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 236 | background-color: #F5F5F5; 237 | } 238 | &::-webkit-scrollbar { 239 | width: 12px; 240 | background-color: #F5F5F5; 241 | } 242 | &::-webkit-scrollbar-thumb { 243 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 244 | background-color: #555; 245 | } 246 | } 247 | 248 | .btn-group.dropup { 249 | position: fixed; 250 | left: 0px; 251 | bottom: 0; 252 | } 253 | 254 | } 255 | 256 | .navbar-brand { 257 | img { 258 | float: left; 259 | top: -10px; 260 | position: relative; 261 | margin-right: 10px; 262 | } 263 | 264 | } 265 | 266 | .navbar-text { 267 | padding-right: 110px; 268 | } 269 | -------------------------------------------------------------------------------- /src/assets/css/styles.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; 2 | 3 | @import "~bootstrap-sass/assets/stylesheets/_bootstrap"; 4 | @import "chat"; 5 | -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/female-avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/female-avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/female-avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/avatars/female-avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/female-avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/male-avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/male-avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/male-avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/avatars/male-avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/avatars/male-avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/logos/Angular2RxJSChatHeaderImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/logos/Angular2RxJSChatHeaderImage.png -------------------------------------------------------------------------------- /src/assets/images/logos/ng-book-2-minibook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/logos/ng-book-2-minibook.png -------------------------------------------------------------------------------- /src/assets/images/readme/full-chat-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/readme/full-chat-preview.png -------------------------------------------------------------------------------- /src/assets/images/readme/ng-book-2-as-book-cover-pigment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/readme/ng-book-2-as-book-cover-pigment.png -------------------------------------------------------------------------------- /src/assets/images/readme/rx-chat-echo-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/readme/rx-chat-echo-bot.png -------------------------------------------------------------------------------- /src/assets/images/readme/rx-chat-models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/readme/rx-chat-models.png -------------------------------------------------------------------------------- /src/assets/images/readme/rx-chat-top-level-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/assets/images/readme/rx-chat-top-level-components.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ng-book/angular2-rxjs-chat/d5e84a9e13548ff4a32e986cf6d9b3fd41a1fc45/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-book 2: AngularReduxChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "import-spacing": true, 15 | "indent": [ 16 | true, 17 | "spaces" 18 | ], 19 | "interface-over-type-literal": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": false, 26 | "member-ordering": [ 27 | true, 28 | "static-before-instance", 29 | "variables-before-functions" 30 | ], 31 | "no-arg": true, 32 | "no-bitwise": true, 33 | "no-console": [ 34 | true, 35 | "debug", 36 | "info", 37 | "time", 38 | "timeEnd", 39 | "trace" 40 | ], 41 | "no-construct": true, 42 | "no-debugger": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-empty-interface": true, 46 | "no-eval": true, 47 | "no-inferrable-types": [true, "ignore-params"], 48 | "no-shadowed-variable": true, 49 | "no-string-literal": false, 50 | "no-string-throw": true, 51 | "no-switch-case-fall-through": true, 52 | "no-trailing-whitespace": true, 53 | "no-unused-expression": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "prefer-const": true, 65 | "quotemark": [ 66 | true, 67 | "single" 68 | ], 69 | "radix": true, 70 | "semicolon": [ 71 | "always" 72 | ], 73 | "triple-equals": [ 74 | true, 75 | "allow-null-check" 76 | ], 77 | "typedef-whitespace": [ 78 | true, 79 | { 80 | "call-signature": "nospace", 81 | "index-signature": "nospace", 82 | "parameter": "nospace", 83 | "property-declaration": "nospace", 84 | "variable-declaration": "nospace" 85 | } 86 | ], 87 | "typeof-compare": true, 88 | "unified-signatures": true, 89 | "variable-name": false, 90 | "whitespace": [ 91 | true, 92 | "check-branch", 93 | "check-decl", 94 | "check-operator", 95 | "check-separator", 96 | "check-type" 97 | ], 98 | 99 | "directive-selector": [true, "attribute", "app", "camelCase"], 100 | "use-input-property-decorator": true, 101 | "use-output-property-decorator": true, 102 | "use-host-property-decorator": true, 103 | "no-input-rename": true, 104 | "no-output-rename": true, 105 | "use-life-cycle-interface": true, 106 | "use-pipe-transform-interface": true, 107 | "component-class-suffix": true, 108 | "directive-class-suffix": true, 109 | "no-access-missing-member": true, 110 | "templates-use-public": true, 111 | "invoke-injectable": true 112 | } 113 | } 114 | --------------------------------------------------------------------------------