├── .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 |
3 |
4 |
5 | # Angular 2 RxJS Chat [](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 |
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 |
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 |
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 |
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 |
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 |
2 |
3 |
9 |
10 |
11 | Messages {{ unreadMessagesCount }}
12 |
13 |
14 |
15 |
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 |
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 |
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 |
--------------------------------------------------------------------------------