├── .editorconfig ├── .gitignore ├── README.adoc ├── chat-api ├── .eslintrc.js ├── .prettierrc ├── README.adoc ├── docker-compose-files │ └── initdbs.js ├── docker-compose.yaml ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── controllers │ │ ├── messages │ │ │ ├── messages.controller.spec.ts │ │ │ └── messages.controller.ts │ │ └── rooms │ │ │ ├── rooms.controller.spec.ts │ │ │ └── rooms.controller.ts │ ├── gateways │ │ └── messages │ │ │ └── messages.gateway.ts │ ├── main.hmr.ts │ ├── main.ts │ └── models │ │ ├── message.model.ts │ │ ├── room.model.ts │ │ └── user.model.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── webpack.config.js ├── chat-client ├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── README.adoc ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json ├── ionic.config.json ├── ionic.starter.json ├── karma.conf.js ├── package.json ├── pnpm-lock.yaml ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── models │ │ │ ├── message.ts │ │ │ ├── room.ts │ │ │ └── user.ts │ │ ├── pages │ │ │ ├── chat-room │ │ │ │ ├── chat-room.module.ts │ │ │ │ ├── chat-room.page.html │ │ │ │ ├── chat-room.page.scss │ │ │ │ ├── chat-room.page.spec.ts │ │ │ │ └── chat-room.page.ts │ │ │ ├── home │ │ │ │ ├── home-routing.module.ts │ │ │ │ ├── home.module.ts │ │ │ │ ├── home.page.html │ │ │ │ ├── home.page.scss │ │ │ │ ├── home.page.spec.ts │ │ │ │ └── home.page.ts │ │ │ └── select-room │ │ │ │ ├── select-room.module.ts │ │ │ │ ├── select-room.page.html │ │ │ │ ├── select-room.page.scss │ │ │ │ ├── select-room.page.spec.ts │ │ │ │ └── select-room.page.ts │ │ └── services │ │ │ ├── messages.service.spec.ts │ │ │ ├── messages.service.ts │ │ │ ├── rooms.service.spec.ts │ │ │ └── rooms.service.ts │ ├── assets │ │ ├── icon │ │ │ └── favicon.png │ │ └── shapes.svg │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── global.scss │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── test.ts │ ├── theme │ │ └── variables.scss │ └── zone-flags.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── chat-demo.gif ├── highlight.min.js ├── index.html └── styles └── github.min.css /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | dist/ 32 | .angular/ 33 | 34 | .DS_Store 35 | Thumbs.db 36 | UserInterfaceState.xcuserstate 37 | /*/dist/ 38 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Simple Chat example using Ionic, Nest, Socket.IO and MongoDB 2 | :source-highlighter: highlightjs 3 | :highlightjsdir: . 4 | :nofooter: 5 | :icons: font 6 | :toc: left 7 | :prev_section: 01-simple-chat 8 | 9 | **** 10 | source code: https://github.com/nest-ionic-examples/02-chat-with-db 11 | **** 12 | 13 | In https://nest-ionic-examples.github.io/01-simple-chat[previous tutorial] we created a simple chat application, in this tutorial we are going to add support for MongoDb database to store messages, users and rooms. 14 | 15 | image::chat-demo.gif[] 16 | 17 | include::chat-api/README.adoc[] 18 | 19 | include::chat-client/README.adoc[] 20 | 21 | == Conclusion 22 | 23 | As you can see adding MongoDB to our project was relatively simple. Furthermore, `@nestjs/mongoose` give us the huge ability to connect and handle the database without writing too much code. 24 | 25 | Now this application looks more real since we can store all the messages, rooms, and users in our database. This leads to the fact that now we can create different chat-rooms with separate conversations. 26 | 27 | To finish I just want to point that we need to add some sort of authentication and authorization system. So that, only users with required permissions can access rooms and messages. We will do that in next tutorials. 28 | 29 | == Comments 30 | 31 | ++++ 32 |
33 | 46 | 47 | ++++ 48 | -------------------------------------------------------------------------------- /chat-api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /chat-api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /chat-api/README.adoc: -------------------------------------------------------------------------------- 1 | == Nest Application 2 | 3 | === Database Setup 4 | 5 | In this tutorial we are going to use MongoDb as database however you could use another database with some little tweaks. We are going to explore two ways to add MongoDb locally. 6 | 7 | [NOTE] 8 | ==== 9 | Before continue with this part, in the console go to `chat-api` folder: 10 | 11 | ```sh 12 | cd chat-api 13 | ``` 14 | ==== 15 | 16 | ==== Brew Installation 17 | 18 | The first one is by using https://brew.sh/[brew] or http://linuxbrew.sh/[linuxbrew], so you could run: 19 | 20 | ```sh 21 | brew install mongodb 22 | ``` 23 | 24 | then you could just start it running 25 | 26 | ```sh 27 | brew services start mongo 28 | ``` 29 | 30 | Then start the mongo shell pointing to the `chat` database: 31 | 32 | ```sh 33 | mongo chat 34 | ``` 35 | 36 | After you start the mongo-shell you need to add user `chat-admin`, set password `password123` and add the role `dbAdmin` for `chat` database. so run next commands in the console: 37 | 38 | [source,sh,options="nowrap"] 39 | ---- 40 | db.createUser({user: 'chat-admin', pwd: 'password123', roles: [{role: 'dbAdmin', db: 'chat'}]}) 41 | ---- 42 | 43 | then you are going to get something like next: 44 | 45 | [source,sh,options=nowrap"] 46 | ---- 47 | Successfully added user: { 48 | "user" : "chat-admin", 49 | "roles" : [ 50 | { 51 | "role" : "dbAdmin", 52 | "db" : "chat" 53 | } 54 | ] 55 | } 56 | ---- 57 | 58 | then just press combination `ctrl + C` to stop mongo shell. 59 | 60 | [NOTE] 61 | ==== 62 | If you want to start another instance of mongodb in the same port, for example with `docker-compose`, then you need to stop this one. Hence, you could run: 63 | 64 | ```sh 65 | brew services stop mongo 66 | ``` 67 | ==== 68 | 69 | 70 | ==== Docker Compose Setup 71 | 72 | The second way is to use https://www.docker.com/get-started[docker] and https://docs.docker.com/compose/gettingstarted/[docker-compose]. To do that, we need to create a `docker-compose.yaml` file and add next code: 73 | 74 | [source,yaml,options="nowrap"] 75 | ---- 76 | include::docker-compose.yaml[] 77 | ---- 78 | <1> whenever we run `docker-compose up mongo` the database will run and listen on port `27017`. 79 | <2> We create a virtual volume from `docker-compose-files` folder created in the host machine and share it inside the container as `docker-entrypoint-initdb.d`. In this folder we will put initialization scripts to start the database with preloaded data. 80 | <3> We set the default database that will be used by the application 81 | 82 | As you can see in the previous file `docker-compose` needs initialization files to start the database, so we will create file `docker-compose-files/initdbs.js` which will contain the initialization script. Then add next code to it: 83 | 84 | [source,yaml,options="nowrap"] 85 | ---- 86 | include::docker-compose-files/initdbs.js[] 87 | ---- 88 | <1> creates a new user `chat-admin`, sets password `password123` and add the role `dbAdmin` for `chat` database. 89 | 90 | There are several advantages of using `docker-compose` over local `brew` installation: 91 | 92 | 1. We can create a new MongoDB database for every project without affecting others. 93 | 2. We could destroy the database data just running: `docker-compose down -v` and restart it running `docker-compose up mongo`. This is very important for integration testing. 94 | 3. We can specify NodeJs and MongoDB version we are working, so others just need to run `docker-compose up mongo` to have the same environment as us. 95 | 96 | === Nestjs/Mongoose 97 | 98 | In order to add support for mongodb database, Nest comes with the ready to use `@nestjs/mongoose` package. This is one of the most mature available so far. Since it's written in TypeScript, it works pretty well with the Nest framework. 99 | 100 | Firstly, we need to install all of the required dependencies: 101 | 102 | [source,sh,options="nowrap"] 103 | ---- 104 | npm i -s @nestjs/mongoose mongoose 105 | ---- 106 | 107 | Once the installation process is completed, we can import the `MongooseModule` into the root `ApplicationModule`. 108 | 109 | [source,ts,options="nowrap"] 110 | ---- 111 | include::src/app.module.ts[tags=!*] 112 | ---- 113 | <1> Import `MongooseModule` from `@nestjs/mongoose` package 114 | <2> Add `MongooseModule` configurations for mongo database connection 115 | <3> Add models to `MongooseModule` so they can be injected later in the application components 116 | 117 | Now we need to create three models `Message`, `Room`, and `User`. 118 | 119 | .src/models/message.model.ts 120 | [source,ts,options="nowrap"] 121 | ---- 122 | include::src/models/message.model.ts[] 123 | ---- 124 | 125 | .src/models/room.model.ts 126 | [source,ts,options="nowrap"] 127 | ---- 128 | include::src/models/room.model.ts[] 129 | ---- 130 | 131 | .src/models/user.model.ts 132 | [source,ts,options="nowrap"] 133 | ---- 134 | include::src/models/user.model.ts[] 135 | ---- 136 | === Modify Gateways and Controllers 137 | 138 | After that we need to modify `messages.gateway.ts`, so we inject each db model: 139 | 140 | [source,ts,options="nowrap"] 141 | ---- 142 | include::src/gateways/messages/messages.gateway.ts[] 143 | ---- 144 | <1> Inject mongoose models for: `Message`, `Room`, and `User`. 145 | <2> Handles user disconnection, it sends an event that the user is disconnected. 146 | <3> Handles subscription to `enter-chat-room` event, which is in charge of adding user to a chat room, if the user doesn't exist then create a new one. 147 | <4> Handles subscription to `leave-chat-room` event, which remove user from the chat room and emits `users-changed` event to all users of the chat-room 148 | <5> Handles subscription to `add-message` event, which is in charge of adding messages coming from users in the chat room 149 | 150 | Then we need to modify `rooms.controller.ts`, so we inject `Room` model into it: 151 | 152 | [source,ts,options="nowrap"] 153 | ---- 154 | include::src/controllers/rooms/rooms.controller.ts[] 155 | ---- 156 | <1> Inject `Room` mongoose model 157 | <2> Handles `GET` request for `api/rooms`. This request could contains a query parameter called `q`. This could contain a partial `room.name` value so users can query to the database with values that matches the partial value. If the query parameter is not present then it will return the full list of rooms. 158 | <3> Handles `GET` request for `api/rooms/:id`. Finds and returns the full information of the room matching that `id`. 159 | <4> Handles the `POST` request for `api/rooms`. If the item contains an `_id` value, it updates any previous created room. if not, it creates a new room with the passed values. Finally, it returns the saved value to the client. 160 | 161 | Finally we need to add `messages.controller.ts`. To do that you should run: 162 | 163 | ``` 164 | nest g co controllers/messages 165 | ``` 166 | 167 | Then modify `messages.controller.ts` with next code: 168 | 169 | [source,ts,options="nowrap"] 170 | ---- 171 | include::src/controllers/messages/messages.controller.ts[] 172 | ---- 173 | <1> Inject `Message` model 174 | <2> Handles `GET` request for `api/messages`. This request could contains a query parameter called `where`. This could contain any query so users can query to the database with values that matches it, for example `{owner: {_id: '123'}}`. If the `where` query parameter is not present then it will return the full list of rooms. 175 | 176 | [IMPORTANT] 177 | ==== 178 | At the moment queries of controllers does not have any validation so it's pretty dangerous to use it like that. 179 | 180 | Also Gateway and Controllers need an authentication and permission system so only users with needed permissions can access and modify data. 181 | 182 | In next tutorials I will add a better handling of it. 183 | ==== 184 | -------------------------------------------------------------------------------- /chat-api/docker-compose-files/initdbs.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: 'chat-admin', 3 | pwd: 'password123', 4 | roles: [{role: 'dbAdmin', db: 'chat'}] 5 | }); // <1> 6 | -------------------------------------------------------------------------------- /chat-api/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | mongo: 4 | image: mongo 5 | ports: 6 | - 27017:27017 # <1> 7 | volumes: 8 | - ./docker-compose-files:/docker-entrypoint-initdb.d # <2> 9 | environment: 10 | MONGO_INITDB_DATABASE: chat # <3> 11 | -------------------------------------------------------------------------------- /chat-api/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /chat-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-api", 3 | "version": "0.0.0", 4 | "description": "Chat Api", 5 | "author": "Luis Vargas", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "dc:up:mongo": "docker-compose up mongo", 23 | "dc:start:mongo": "docker-compose start mongo", 24 | "dc:down": "docker-compose down -v --remove-orphans", 25 | "docker:stop:all": "docker stop $(docker ps -q)" 26 | }, 27 | "dependencies": { 28 | "@nestjs/common": "^9.0.0", 29 | "@nestjs/core": "^9.0.0", 30 | "@nestjs/mongoose": "^9.2.0", 31 | "@nestjs/platform-express": "^9.0.0", 32 | "@nestjs/platform-socket.io": "^9.0.7", 33 | "@nestjs/websockets": "^9.0.7", 34 | "mongoose": "^6.5.0", 35 | "reflect-metadata": "^0.1.13", 36 | "rxjs": "^7.2.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "^9.0.0", 40 | "@nestjs/schematics": "^9.0.0", 41 | "@nestjs/testing": "^9.0.0", 42 | "@types/express": "^4.17.13", 43 | "@types/jest": "28.1.4", 44 | "@types/node": "^16.0.0", 45 | "@types/supertest": "^2.0.11", 46 | "@typescript-eslint/eslint-plugin": "^5.0.0", 47 | "@typescript-eslint/parser": "^5.0.0", 48 | "eslint": "^8.0.1", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "jest": "28.1.2", 52 | "prettier": "^2.3.2", 53 | "rimraf": "^3.0.2", 54 | "source-map-support": "^0.5.20", 55 | "supertest": "^6.1.3", 56 | "ts-jest": "28.0.5", 57 | "ts-loader": "^9.2.3", 58 | "ts-node": "^10.0.0", 59 | "tsconfig-paths": "4.0.0", 60 | "typescript": "^4.3.5", 61 | "webpack": "^5.74.0" 62 | }, 63 | "jest": { 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "ts" 68 | ], 69 | "rootDir": "src", 70 | "testRegex": ".*\\.spec\\.ts$", 71 | "transform": { 72 | "^.+\\.(t|j)s$": "ts-jest" 73 | }, 74 | "collectCoverageFrom": [ 75 | "**/*.(t|j)s" 76 | ], 77 | "coverageDirectory": "../coverage", 78 | "testEnvironment": "node" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /chat-api/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.root()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /chat-api/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | root() { 10 | return this.appService.root(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chat-api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { MessagesGateway } from './gateways/messages/messages.gateway'; 5 | // tag::import-messages-controller[] 6 | import { MessagesController } from './controllers/messages/messages.controller'; 7 | // end::import-messages-controller[] 8 | import { RoomsController } from './controllers/rooms/rooms.controller'; 9 | import { MongooseModule } from "@nestjs/mongoose"; // <1> 10 | import { Message, MessageSchema } from './models/message.model'; 11 | import { Room, RoomSchema } from './models/room.model'; 12 | import { User, UserSchema } from './models/user.model'; 13 | 14 | @Module({ 15 | imports: [ 16 | MongooseModule.forRoot('mongodb://chat-admin:password123@localhost/chat', {}), // <2> 17 | MongooseModule.forFeature([ 18 | {name: Message.name, schema: MessageSchema}, 19 | {name: Room.name, schema: RoomSchema}, 20 | {name: User.name, schema: UserSchema} 21 | ]), // <3> 22 | ], 23 | controllers: [ 24 | AppController, 25 | RoomsController, 26 | // tag::messages-controller[] 27 | MessagesController, 28 | // end::messages-controller[] 29 | ], 30 | providers: [AppService, MessagesGateway], 31 | }) 32 | export class AppModule { 33 | } 34 | -------------------------------------------------------------------------------- /chat-api/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | root(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /chat-api/src/controllers/messages/messages.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessagesController } from './messages.controller'; 3 | 4 | describe('Messages Controller', () => { 5 | let module: TestingModule; 6 | 7 | beforeAll(async () => { 8 | module = await Test.createTestingModule({ 9 | controllers: [MessagesController], 10 | }).compile(); 11 | }); 12 | it('should be defined', () => { 13 | const controller: MessagesController = module.get(MessagesController); 14 | expect(controller).toBeDefined(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /chat-api/src/controllers/messages/messages.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { Message } from '../../models/message.model'; 3 | import {Model} from "mongoose"; 4 | import {InjectModel} from "@nestjs/mongoose"; 5 | 6 | @Controller('api/messages') 7 | export class MessagesController { 8 | constructor(@InjectModel(Message.name) private readonly model: Model) {} // <1> 9 | 10 | @Get() 11 | find(@Query('where') where) { // <2> 12 | where = JSON.parse(where || '{}'); 13 | return this.model.find(where).populate('owner').exec(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chat-api/src/controllers/rooms/rooms.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RoomsController } from './rooms.controller'; 3 | 4 | describe('Rooms Controller', () => { 5 | let module: TestingModule; 6 | 7 | beforeAll(async () => { 8 | module = await Test.createTestingModule({ 9 | controllers: [RoomsController], 10 | }).compile(); 11 | }); 12 | it('should be defined', () => { 13 | const controller: RoomsController = module.get(RoomsController); 14 | expect(controller).toBeDefined(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /chat-api/src/controllers/rooms/rooms.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; 2 | import { Room } from '../../models/room.model'; 3 | import {Model} from "mongoose"; 4 | import {InjectModel} from "@nestjs/mongoose"; 5 | 6 | @Controller('api/rooms') 7 | export class RoomsController { 8 | constructor(@InjectModel(Room.name) private readonly model: Model) {} // <1> 9 | 10 | @Get() 11 | find(@Query('q') q) { // <2> 12 | if (q) return this.model.find({name: {$regex: new RegExp(`.*${q}.*`)}}); 13 | else return this.model.find(); 14 | } 15 | 16 | @Get('/:id') 17 | findById(@Param('id') id: string) { // <3> 18 | return this.model.findById(id); 19 | } 20 | 21 | @Post() 22 | save(@Body() item: Room) { // <4> 23 | return item._id 24 | ? this.model.findByIdAndUpdate(item._id, item, {new: true}) 25 | : this.model.create(item); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chat-api/src/gateways/messages/messages.gateway.ts: -------------------------------------------------------------------------------- 1 | import {OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer} from '@nestjs/websockets'; 2 | import { Socket } from 'socket.io'; 3 | import { Message } from '../../models/message.model'; 4 | import { User } from '../../models/user.model'; 5 | import { Room } from '../../models/room.model'; 6 | import {InjectModel} from "@nestjs/mongoose"; 7 | import {Model} from "mongoose"; 8 | import {Server} from "socket.io"; 9 | 10 | @WebSocketGateway({cors: '*:*'}) 11 | export class MessagesGateway implements OnGatewayDisconnect { 12 | 13 | constructor(@InjectModel(Message.name) private readonly messagesModel: Model, 14 | @InjectModel(Room.name) private readonly roomsModel: Model, 15 | @InjectModel(User.name) private readonly usersModel: Model) { // <1> 16 | } 17 | 18 | @WebSocketServer() 19 | server: Server; 20 | 21 | async handleDisconnect(client: Socket) { // <2> 22 | const user = await this.usersModel.findOne({clientId: client.id}); 23 | if (user) { 24 | this.server.emit('users-changed', {user: user.nickname, event: 'left'}); 25 | user.clientId = null; 26 | await this.usersModel.findByIdAndUpdate(user._id, user); 27 | } 28 | } 29 | 30 | @SubscribeMessage('enter-chat-room') // <3> 31 | async enterChatRoom(client: Socket, data: { nickname: string, roomId: string }) { 32 | let user = await this.usersModel.findOne({nickname: data.nickname}); 33 | if (!user) { 34 | user = await this.usersModel.create({nickname: data.nickname, clientId: client.id}); 35 | } else { 36 | user.clientId = client.id; 37 | user = await this.usersModel.findByIdAndUpdate(user._id, user, {new: true}); 38 | } 39 | client.join(data.roomId); 40 | client.broadcast.to(data.roomId) 41 | .emit('users-changed', {user: user.nickname, event: 'joined'}); // <3> 42 | } 43 | 44 | @SubscribeMessage('leave-chat-room') // <4> 45 | async leaveChatRoom(client: Socket, data: { nickname: string, roomId: string }) { 46 | const user = await this.usersModel.findOne({nickname: data.nickname}); 47 | client.broadcast.to(data.roomId).emit('users-changed', {user: user.nickname, event: 'left'}); // <3> 48 | client.leave(data.roomId); 49 | } 50 | 51 | @SubscribeMessage('add-message') // <5> 52 | async addMessage(client: Socket, message: Message) { 53 | message.owner = await this.usersModel.findOne({clientId: client.id}); 54 | message.created = new Date(); 55 | message = await this.messagesModel.create(message); 56 | this.server.in(message.room as string).emit('message', message); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /chat-api/src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | 16 | bootstrap(); 17 | -------------------------------------------------------------------------------- /chat-api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as cors from 'cors'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.use(cors()); 8 | await app.listen(3000); 9 | } 10 | 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /chat-api/src/models/message.model.ts: -------------------------------------------------------------------------------- 1 | import {User} from './user.model'; 2 | import {Room} from './room.model'; 3 | import {ObjectID} from 'bson'; 4 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose"; 5 | import {Types} from "mongoose"; 6 | 7 | @Schema() 8 | export class Message { 9 | 10 | _id: ObjectID | string; 11 | 12 | @Prop({required: true}) 13 | text: string; 14 | 15 | @Prop({required: true}) 16 | created: Date; 17 | 18 | @Prop({required: true, ref: 'User', type: Types.ObjectId}) 19 | owner: User; 20 | 21 | @Prop({required: true, ref: 'Room', type: Types.ObjectId}) 22 | room: Room | string; 23 | } 24 | 25 | export const MessageSchema = SchemaFactory.createForClass(Message) 26 | -------------------------------------------------------------------------------- /chat-api/src/models/room.model.ts: -------------------------------------------------------------------------------- 1 | import {Message} from './message.model'; 2 | import {User} from './user.model'; 3 | import {ObjectID} from 'bson'; 4 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose"; 5 | import {Types} from "mongoose"; 6 | 7 | @Schema() 8 | export class Room { 9 | _id: ObjectID | string; 10 | 11 | @Prop({required: true, maxlength: 20, minlength: 5}) 12 | name: string; 13 | 14 | @Prop({type: [{type: Types.ObjectId, ref: 'Message'}]}) 15 | messages: Message[]; 16 | 17 | @Prop({type: [{type: Types.ObjectId, ref: 'User'}]}) 18 | connectedUsers: User[]; 19 | } 20 | 21 | export const RoomSchema = SchemaFactory.createForClass(Room) 22 | -------------------------------------------------------------------------------- /chat-api/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import {Message} from './message.model'; 2 | import {Room} from './room.model'; 3 | import {ObjectID} from 'bson'; 4 | import {Types} from "mongoose"; 5 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose"; 6 | 7 | @Schema() 8 | export class User { 9 | _id?: ObjectID | string; 10 | 11 | @Prop({required: true, maxlength: 20, minlength: 5}) 12 | nickname: string; 13 | 14 | @Prop({required: true}) 15 | clientId: string; 16 | 17 | @Prop({type: [{type: Types.ObjectId, ref: 'Message'}]}) 18 | messages?: Message[]; 19 | 20 | @Prop({type: [{type: Types.ObjectId, ref: 'Room'}]}) 21 | joinedRooms?: Room[]; 22 | } 23 | 24 | export const UserSchema = SchemaFactory.createForClass(User) 25 | -------------------------------------------------------------------------------- /chat-api/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /chat-api/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /chat-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /chat-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chat-api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /chat-api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [] 55 | } 56 | -------------------------------------------------------------------------------- /chat-api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /chat-client/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /chat-client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /chat-client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/ng-cli-compat", 13 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 14 | "plugin:@angular-eslint/template/process-inline-templates" 15 | ], 16 | "rules": { 17 | "@angular-eslint/component-class-suffix": [ 18 | "error", 19 | { 20 | "suffixes": ["Page", "Component"] 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "app", 28 | "style": "kebab-case" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "type": "attribute", 35 | "prefix": "app", 36 | "style": "camelCase" 37 | } 38 | ], 39 | "no-underscore-dangle": "off" 40 | } 41 | }, 42 | { 43 | "files": ["*.html"], 44 | "extends": ["plugin:@angular-eslint/template/recommended"], 45 | "rules": {} 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /chat-client/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | .tmp 7 | *.tmp 8 | *.tmp.* 9 | *.sublime-project 10 | *.sublime-workspace 11 | .DS_Store 12 | Thumbs.db 13 | UserInterfaceState.xcuserstate 14 | $RECYCLE.BIN/ 15 | 16 | *.log 17 | log.txt 18 | npm-debug.log* 19 | 20 | /.angular 21 | /.idea 22 | /.ionic 23 | /.sass-cache 24 | /.sourcemaps 25 | /.versions 26 | /.vscode 27 | /coverage 28 | /dist 29 | /node_modules 30 | /platforms 31 | /plugins 32 | /www 33 | -------------------------------------------------------------------------------- /chat-client/README.adoc: -------------------------------------------------------------------------------- 1 | == Ionic Application 2 | 3 | In the previous tutorial inside our Ionic chat app we created 2 screens: On the first screen we picked a name and *join the chat*, on the second screen we show the actual *chatroom with messages*. In this tutorial we are going to modify the first screen, so after setting the nickname we go to another page where we select the chat room from a list, and then in the last screen we will only show messages for that specified chat room. 4 | 5 | [NOTE] 6 | ==== 7 | Before continue with this part, in the console go to `chat-client` folder: 8 | 9 | ```sh 10 | cd chat-clent 11 | ``` 12 | ==== 13 | 14 | === Adding Models 15 | 16 | Before continue it is good idea to add the models to our app, so create next files: 17 | 18 | .src/app/models/message.ts 19 | [source,ts,options="nowrap] 20 | ---- 21 | include::src/app/models/message.ts[] 22 | ---- 23 | 24 | .src/app/models/room.ts 25 | [source,ts,options="nowrap] 26 | ---- 27 | include::src/app/models/room.ts[] 28 | ---- 29 | 30 | .src/app/models/user.ts 31 | [source,ts,options="nowrap] 32 | ---- 33 | include::src/app/models/user.ts[] 34 | ---- 35 | 36 | As you can see those files are only interfaces that have the same attributes as the server entities. 37 | 38 | NOTE: Even though there is a way to put this models in a separate library and share this library between the server and client we are not going to do it in this tutorial. 39 | 40 | === Joining a Chatroom 41 | 42 | So now we need to add a way to select the chat room after setting the nickname. To do that we should modify the file `src/app/pages/home/home.ts` with the next code: 43 | 44 | [source,ts,options="nowrap"] 45 | ---- 46 | include::src/app/pages/home/home.page.ts[] 47 | ---- 48 | <1> instead setting the nickname in the url-path we save it into the local storage variable so we can use it later. 49 | <2> we redirect the user to the `select-room` page 50 | 51 | The template file will be the same, so you can keep the next code: 52 | 53 | [source,html,options="nowrap"] 54 | ---- 55 | include::src/app/pages/home/home.page.html[] 56 | ---- 57 | 58 | === Building the Room Selection Functionality 59 | 60 | In this page the user will select the chat room he will join, so we need a selection list and a filter box. The first step will be to create the page running next command: 61 | 62 | 63 | ```sh 64 | ionic g page pages/select-room 65 | ``` 66 | 67 | then we modify `select-room.page.html` to contain next code: 68 | 69 | [source,html,options="nowrap"] 70 | ---- 71 | include::src/app/pages/select-room/select-room.page.html[] 72 | ---- 73 | <1> In the header we show the title and a search-box which executes the method `searchRoom` whenever user types-in and the debounce time has elapsed. 74 | <2> The content shows the list of rooms filtered by the search box. Whenever the user clicks on any of the items of the list, it will be redirected to the `chat-room` page. 75 | <3> The footer contains a text-box that receive the name of a new chat-room and a plus button which executes the method `addRoom`. 76 | 77 | Before modifying the `select-room.page.ts` file, it will be needed to add the `debounce-decorator-ts` package. To do it, run next command at the root directory of `chat-client` app: 78 | 79 | ``` 80 | npm i -s debounce-decorator-ts 81 | ``` 82 | 83 | then we modify `select-room.page.ts` to contain next code: 84 | 85 | [source,html,options="nowrap"] 86 | ---- 87 | include::src/app/pages/select-room/select-room.page.ts[] 88 | ---- 89 | <1> In the `constructor` we inject 90 | <2> In the `ngOnInit` method we search for all the rooms 91 | <3> The `searchRoom` method is in charge of searching for rooms in dependence of the parameter `q`. This method calls `roomsService.find` method passing parameter `q` to it. After receiving the rooms list, it fills a local public array to be used by the html template. 92 | <4> The `joinRoom` method navigates to `chat-room/:id`. That `id` parameter is later used in the `chat-room` page. 93 | <5> The `addRoom` method calls `roomsService.save` method which sends the information of the new room to the server. After saving the value, it receives the new value and adds it to the `rooms` local public variable. 94 | 95 | === Modifying the Chat Functionality 96 | 97 | To receive new chat messages inside the room we have to listen for `message` socket event which receive the messages from the server. 98 | 99 | Whenever we get such a message we simply push the new message to an array of messages. Remember, since now we have a database to save historic data, we are going to load it. 100 | 101 | Sending a new message is almost the same as before, we simply emit our event to the server with the right type. 102 | 103 | Finally, we also listen to the events of users joining and leaving the room and display a little toast whenever someone comes in or leaves the room. It’s the same logic again, with the `socket.on()` we can listen to all the events broadcasted from our server! 104 | 105 | Go ahead and modify `chat-room.ts`: 106 | 107 | [source,ts,options="nowrap"] 108 | ---- 109 | include::src/app/pages/chat-room/chat-room.page.ts[] 110 | ---- 111 | <1> We import models and services 112 | <2> We inject services `MessagesService` and `RoomsService` in the constructor 113 | <3> Instead getting `nickname` from url, we now get it from `sessionStorage`. 114 | <4> We get the roomId from the value coming from the route path param. 115 | <5> We emit that the user has entered to the chat-room. 116 | <6> Then we get the full information of the chat-room using `roomsService.findById` method. 117 | <7> After receiving the full chat-room info, we set it in a local public variable to be accessible to the html template. 118 | <8> Then we find all the messages of the chat-room using query: `{where: JSON.stringify({room: this.room._id})}` 119 | <9> After getting all the messages of the chat-room, we set them in a local public array to be accessible to the html template. 120 | <10> In the `ngOnDestroy` method we need to unsubscribe all the subscriptions, remove listeners for `socket.io` events, and emit `leave-chat-room` event so the server can know when a user has left the room. This method is always call whenever the user goes back using the back button. 121 | <11> We emit the `add-message` event with the message text and the room id. 122 | 123 | And also `chat-room.html`: 124 | 125 | [source,html,options="nowrap"] 126 | ---- 127 | include::src/app/pages/chat-room/chat-room.page.html[] 128 | ---- 129 | <1> we have to go back to `select-room` page instead `home` page 130 | <2> Instead of just showing `Chat` in the header, we now show `Room: `. 131 | <3> now we compare `message.owner.nickname` instead comparing `message.from` 132 | 133 | Now launch your app and make sure your backend is up and running! 134 | 135 | For testing, you can open a browser and another incognito browser like in my example at the top to chat with yourself. 136 | -------------------------------------------------------------------------------- /chat-client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "www", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "src/assets", 25 | "output": "assets" 26 | }, 27 | { 28 | "glob": "**/*.svg", 29 | "input": "node_modules/ionicons/dist/ionicons/svg", 30 | "output": "./svg" 31 | } 32 | ], 33 | "styles": ["src/theme/variables.scss", "src/global.scss"], 34 | "scripts": [], 35 | "aot": false, 36 | "vendorChunk": true, 37 | "extractLicenses": false, 38 | "buildOptimizer": false, 39 | "sourceMap": true, 40 | "optimization": false, 41 | "namedChunks": true 42 | }, 43 | "configurations": { 44 | "production": { 45 | "fileReplacements": [ 46 | { 47 | "replace": "src/environments/environment.ts", 48 | "with": "src/environments/environment.prod.ts" 49 | } 50 | ], 51 | "optimization": true, 52 | "outputHashing": "all", 53 | "sourceMap": false, 54 | "namedChunks": false, 55 | "aot": true, 56 | "extractLicenses": true, 57 | "vendorChunk": false, 58 | "buildOptimizer": true, 59 | "budgets": [ 60 | { 61 | "type": "initial", 62 | "maximumWarning": "2mb", 63 | "maximumError": "5mb" 64 | } 65 | ] 66 | }, 67 | "ci": { 68 | "progress": false 69 | } 70 | } 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "options": { 75 | "browserTarget": "app:build" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "browserTarget": "app:build:production" 80 | }, 81 | "ci": { 82 | "progress": false 83 | } 84 | } 85 | }, 86 | "extract-i18n": { 87 | "builder": "@angular-devkit/build-angular:extract-i18n", 88 | "options": { 89 | "browserTarget": "app:build" 90 | } 91 | }, 92 | "test": { 93 | "builder": "@angular-devkit/build-angular:karma", 94 | "options": { 95 | "main": "src/test.ts", 96 | "polyfills": "src/polyfills.ts", 97 | "tsConfig": "tsconfig.spec.json", 98 | "karmaConfig": "karma.conf.js", 99 | "styles": [], 100 | "scripts": [], 101 | "assets": [ 102 | { 103 | "glob": "favicon.ico", 104 | "input": "src/", 105 | "output": "/" 106 | }, 107 | { 108 | "glob": "**/*", 109 | "input": "src/assets", 110 | "output": "/assets" 111 | } 112 | ] 113 | }, 114 | "configurations": { 115 | "ci": { 116 | "progress": false, 117 | "watch": false 118 | } 119 | } 120 | }, 121 | "lint": { 122 | "builder": "@angular-eslint/builder:lint", 123 | "options": { 124 | "lintFilePatterns": [ 125 | "src/**/*.ts", 126 | "src/**/*.html" 127 | ] 128 | } 129 | }, 130 | "e2e": { 131 | "builder": "@angular-devkit/build-angular:protractor", 132 | "options": { 133 | "protractorConfig": "e2e/protractor.conf.js", 134 | "devServerTarget": "app:serve" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "devServerTarget": "app:serve:production" 139 | }, 140 | "ci": { 141 | "devServerTarget": "app:serve:ci" 142 | } 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | "cli": { 149 | "schematicCollections": [ 150 | "@ionic/angular-toolkit" 151 | ], 152 | "analytics": false 153 | }, 154 | "schematics": { 155 | "@ionic/angular-toolkit:component": { 156 | "styleext": "scss" 157 | }, 158 | "@ionic/angular-toolkit:page": { 159 | "styleext": "scss" 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /chat-client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /chat-client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should be blank', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('Start with Ionic UI Components'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /chat-client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.deepCss('app-root ion-content')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /chat-client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chat-client/ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-app-base", 3 | "integrations": {}, 4 | "type": "angular" 5 | } 6 | -------------------------------------------------------------------------------- /chat-client/ionic.starter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blank Starter", 3 | "baseref": "main", 4 | "tarignore": [ 5 | "node_modules", 6 | "package-lock.json", 7 | "www" 8 | ], 9 | "scripts": { 10 | "test": "npm run lint && npm run ng -- build --configuration=ci && npm run ng -- build --prod --progress=false && npm run ng -- test --configuration=ci && npm run ng -- e2e --configuration=ci && npm run ng -- g pg my-page --dry-run && npm run ng -- g c my-component --dry-run" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /chat-client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ngv'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /chat-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-app-base", 3 | "version": "0.0.0", 4 | "author": "Ionic Framework", 5 | "homepage": "https://ionicframework.com/", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "^14.0.0", 17 | "@angular/core": "^14.0.0", 18 | "@angular/forms": "^14.0.0", 19 | "@angular/platform-browser": "^14.0.0", 20 | "@angular/platform-browser-dynamic": "^14.0.0", 21 | "@angular/router": "^14.0.0", 22 | "@ionic/angular": "^6.1.9", 23 | "debounce-decorator-ts": "^1.0.4", 24 | "ngx-socket-io": "^4.3.0", 25 | "ngx-validators": "^6.0.1", 26 | "rxjs": "~6.6.0", 27 | "tslib": "^2.2.0", 28 | "zone.js": "~0.11.4" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "^14.0.0", 32 | "@angular-eslint/builder": "~13.0.1", 33 | "@angular-eslint/eslint-plugin": "~13.0.1", 34 | "@angular-eslint/eslint-plugin-template": "~13.0.1", 35 | "@angular-eslint/template-parser": "~13.0.1", 36 | "@angular/cli": "^14.0.0", 37 | "@angular/compiler": "^14.0.0", 38 | "@angular/compiler-cli": "^14.0.0", 39 | "@angular/language-service": "^14.0.0", 40 | "@ionic/angular-toolkit": "^6.0.0", 41 | "@types/jasmine": "~3.6.0", 42 | "@types/jasminewd2": "~2.0.3", 43 | "@types/node": "^12.11.1", 44 | "@typescript-eslint/eslint-plugin": "5.3.0", 45 | "@typescript-eslint/parser": "5.3.0", 46 | "eslint": "^7.6.0", 47 | "eslint-plugin-import": "2.22.1", 48 | "eslint-plugin-jsdoc": "30.7.6", 49 | "eslint-plugin-prefer-arrow": "1.2.2", 50 | "jasmine-core": "~3.8.0", 51 | "jasmine-spec-reporter": "~5.0.0", 52 | "karma": "~6.3.2", 53 | "karma-chrome-launcher": "~3.1.0", 54 | "karma-coverage": "~2.0.3", 55 | "karma-coverage-istanbul-reporter": "~3.0.2", 56 | "karma-jasmine": "~4.0.0", 57 | "karma-jasmine-html-reporter": "^1.5.0", 58 | "protractor": "~7.0.0", 59 | "ts-node": "~8.3.0", 60 | "typescript": "~4.7.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /chat-client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | {path: '', redirectTo: 'home', pathMatch: 'full'}, 6 | // tag::home-route[] 7 | {path: 'home', loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule)}, 8 | // end::home-route[] 9 | {path: 'chat-room', loadChildren: () => import('./pages/chat-room/chat-room.module').then(m => m.ChatRoomPageModule)}, 10 | { 11 | path: 'select-room', 12 | loadChildren: () => import('./pages/select-room/select-room.module').then(m => m.SelectRoomPageModule) 13 | }, 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [ 18 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) 19 | ], 20 | exports: [RouterModule] 21 | }) 22 | export class AppRoutingModule { } 23 | -------------------------------------------------------------------------------- /chat-client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /chat-client/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nest-ionic-examples/02-chat-with-db/349c5ef4e31a5dbaace92f500d0d3733101de6b2/chat-client/src/app/app.component.scss -------------------------------------------------------------------------------- /chat-client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | 8 | beforeEach(waitForAsync(() => { 9 | 10 | TestBed.configureTestingModule({ 11 | declarations: [AppComponent], 12 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 13 | }).compileComponents(); 14 | })); 15 | 16 | it('should create the app', () => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | }); 21 | // TODO: add more tests! 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /chat-client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: 'app.component.html', 6 | styleUrls: ['app.component.scss'], 7 | }) 8 | export class AppComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /chat-client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouteReuseStrategy } from '@angular/router'; 4 | 5 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { SocketIoModule } from 'ngx-socket-io'; // <1> 10 | import { environment } from '../environments/environment'; 11 | import { HttpClientModule } from '@angular/common/http'; // <2> 12 | 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | imports: [ 16 | BrowserModule, 17 | HttpClientModule, 18 | IonicModule.forRoot(), 19 | AppRoutingModule, 20 | SocketIoModule.forRoot(environment.socketIoConfig) // <3> 21 | ], 22 | providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], 23 | bootstrap: [AppComponent], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /chat-client/src/app/models/message.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | import { Room } from './room'; 3 | 4 | export interface Message { 5 | _id?: string; 6 | text?: string; 7 | owner?: User | string; 8 | room?: Room | string; 9 | created?: Date | string; 10 | } 11 | -------------------------------------------------------------------------------- /chat-client/src/app/models/room.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './message'; 2 | import { User } from './user'; 3 | 4 | export interface Room { 5 | _id?: string; 6 | name?: string; 7 | messages?: Message[]; 8 | connectedUsers?: User[]; 9 | } 10 | -------------------------------------------------------------------------------- /chat-client/src/app/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Message } from './message'; 2 | import { Room } from './room'; 3 | 4 | export interface User { 5 | _id?: string; 6 | nickname?: string; 7 | clientId?: string; 8 | messages?: Message[]; 9 | joinedRooms?: Room[]; 10 | } 11 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/chat-room/chat-room.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { ChatRoomPage } from './chat-room.page'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: ':roomId', 13 | component: ChatRoomPage 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | FormsModule, 21 | IonicModule, 22 | RouterModule.forChild(routes) 23 | ], 24 | declarations: [ChatRoomPage] 25 | }) 26 | export class ChatRoomPageModule {} 27 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/chat-room/chat-room.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Room: {{room.name}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | {{ message.owner.nickname }}:
20 | {{ message.text }} 21 |
{{message.created | date:'dd.MM hh:MM'}}
22 |
23 | 24 | 29 | {{ message.owner.nickname }}:
30 | {{ message.text }} 31 |
{{message.created | date:'dd.MM hh:mm'}}
32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/chat-room/chat-room.page.scss: -------------------------------------------------------------------------------- 1 | .message { 2 | padding: 10px !important; 3 | transition: all 250ms ease-in-out !important; 4 | border-radius: 10px !important; 5 | margin-bottom: 4px !important; 6 | } 7 | .my_message { 8 | background-color: var(--ion-color-primary) !important; 9 | color: var(--ion-color-primary-contrast) !important; 10 | 11 | .user_name, .time { 12 | color: var(--ion-color-light-shade); 13 | } 14 | } 15 | .other_message { 16 | background-color: var(--ion-color-light-shade) !important; 17 | color: var(--ion-color-light-contrast) !important; 18 | 19 | .user_name, .time { 20 | color: var(--ion-color-dark-tint); 21 | } 22 | } 23 | .time { 24 | float: right; 25 | font-size: small; 26 | } 27 | .message_row { 28 | background-color: #fff; 29 | } 30 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/chat-room/chat-room.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { ChatRoomPage } from './chat-room.page'; 5 | 6 | describe('ChatRoomPage', () => { 7 | let component: ChatRoomPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ ChatRoomPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(ChatRoomPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/chat-room/chat-room.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Socket } from 'ngx-socket-io'; 4 | import { ToastController } from '@ionic/angular'; 5 | import { Subscription } from 'rxjs'; 6 | import { MessagesService } from '../../services/messages.service'; // <1> 7 | import { RoomsService } from '../../services/rooms.service'; 8 | import { Room } from '../../models/room'; 9 | import { Message } from '../../models/message'; 10 | 11 | @Component({ 12 | selector: 'app-chat-room', 13 | templateUrl: './chat-room.page.html', 14 | styleUrls: ['./chat-room.page.scss'], 15 | }) 16 | export class ChatRoomPage implements OnInit, OnDestroy { 17 | messages: Message[] = []; 18 | nickname = ''; 19 | message = ''; 20 | room: Room = {}; 21 | 22 | subscription: Subscription; 23 | 24 | constructor(private route: ActivatedRoute, 25 | private socket: Socket, 26 | private toastCtrl: ToastController, 27 | private messagesService: MessagesService, 28 | private roomsService: RoomsService) { } // <2> 29 | 30 | ngOnInit() { 31 | this.nickname = sessionStorage.getItem('nickname'); // <3> 32 | 33 | this.subscription = this.route.params.subscribe(params => { 34 | const roomId = params.roomId; // <4> 35 | this.socket.emit('enter-chat-room', {roomId, nickname: this.nickname}); // <5> 36 | this.roomsService.findById(roomId).subscribe(room => { // <6> 37 | this.room = room; // <7> 38 | this.messagesService.find({where: JSON.stringify({room: this.room._id})}).subscribe(messages => { // <8> 39 | this.messages = messages; // <9> 40 | }); 41 | }); 42 | }); 43 | 44 | this.socket.on('message', message => this.messages.push(message)); 45 | 46 | this.socket.on('users-changed', data => { 47 | const user = data.user; 48 | if (data.event === 'left') { 49 | this.showToast('User left: ' + user); 50 | } else { 51 | this.showToast('User joined: ' + user); 52 | } 53 | }); 54 | 55 | } 56 | 57 | ngOnDestroy() { // <10> 58 | this.subscription.unsubscribe(); 59 | this.socket.removeAllListeners('message'); 60 | this.socket.removeAllListeners('users-changed'); 61 | this.socket.emit('leave-chat-room', {roomId: this.room._id, nickname: this.nickname}); 62 | } 63 | 64 | sendMessage() { 65 | this.socket.emit('add-message', {text: this.message, room: this.room._id}); // <11> 66 | this.message = ''; 67 | } 68 | 69 | async showToast(msg) { 70 | const toast = await this.toastCtrl.create({ 71 | message: msg, 72 | duration: 2000 73 | }); 74 | toast.present(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { HomePage } from './home.page'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: HomePage, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class HomePageRoutingModule {} 17 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IonicModule } from '@ionic/angular'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { HomePage } from './home.page'; 6 | 7 | import { HomePageRoutingModule } from './home-routing.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | IonicModule, 14 | HomePageRoutingModule 15 | ], 16 | declarations: [HomePage] 17 | }) 18 | export class HomePageModule {} 19 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Home 5 | 6 | 7 | 8 | 9 | 10 | 11 | Set Nickname 12 | 13 | 14 | Join Chat as {{ nickname }} 15 | 16 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home.page.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { IonicModule } from '@ionic/angular'; 3 | 4 | import { HomePage } from './home.page'; 5 | 6 | describe('HomePage', () => { 7 | let component: HomePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ HomePage ], 13 | imports: [IonicModule.forRoot()] 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(HomePage); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | })); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavController } from '@ionic/angular'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | templateUrl: 'home.page.html', 7 | styleUrls: ['home.page.scss'], 8 | }) 9 | export class HomePage { 10 | nickname = ''; 11 | 12 | constructor(private navController: NavController) { } 13 | 14 | joinChat() { 15 | sessionStorage.setItem('nickname', this.nickname); // <1> 16 | this.navController.navigateRoot(`select-room`); // <2> 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/select-room/select-room.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { SelectRoomPage } from './select-room.page'; 9 | import { HttpClientModule } from '@angular/common/http'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: SelectRoomPage 15 | } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [ 20 | CommonModule, 21 | FormsModule, 22 | ReactiveFormsModule, 23 | HttpClientModule, 24 | IonicModule, 25 | RouterModule.forChild(routes) 26 | ], 27 | declarations: [SelectRoomPage] 28 | }) 29 | export class SelectRoomPageModule {} 30 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/select-room/select-room.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Select Room 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{room.name}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/select-room/select-room.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nest-ionic-examples/02-chat-with-db/349c5ef4e31a5dbaace92f500d0d3733101de6b2/chat-client/src/app/pages/select-room/select-room.page.scss -------------------------------------------------------------------------------- /chat-client/src/app/pages/select-room/select-room.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { SelectRoomPage } from './select-room.page'; 5 | 6 | describe('SelectRoomPage', () => { 7 | let component: SelectRoomPage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ SelectRoomPage ], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 14 | }) 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(SelectRoomPage); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /chat-client/src/app/pages/select-room/select-room.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Room } from '../../models/room'; 3 | import { RoomsService } from '../../services/rooms.service'; 4 | import { debounceFn } from 'debounce-decorator-ts'; 5 | import { NavController } from '@ionic/angular'; 6 | 7 | @Component({ 8 | selector: 'app-select-room', 9 | templateUrl: './select-room.page.html', 10 | styleUrls: ['./select-room.page.scss'], 11 | }) 12 | export class SelectRoomPage implements OnInit { 13 | 14 | rooms: Room[]; 15 | 16 | roomName: string; 17 | 18 | constructor(private roomsService: RoomsService, 19 | private navController: NavController) { } // <1> 20 | 21 | ngOnInit() { 22 | this.searchRoom(''); // <2> 23 | } 24 | 25 | @debounceFn(500) 26 | searchRoom(q: string) { // <3> 27 | const params: any = {}; 28 | if (q) { params.q = q; } 29 | this.roomsService.find(params).subscribe(rooms => this.rooms = rooms); 30 | } 31 | 32 | joinRoom(room: Room) { // <4> 33 | this.navController.navigateRoot('chat-room/' + room._id); 34 | } 35 | 36 | addRoom() { // <5> 37 | this.roomsService.save({name: this.roomName}).subscribe(room => { 38 | this.roomName = ''; 39 | this.rooms.push(room); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /chat-client/src/app/services/messages.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { MessagesService } from './messages.service'; 4 | 5 | describe('MessagesService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [MessagesService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([MessagesService], (service: MessagesService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /chat-client/src/app/services/messages.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { environment } from '../../environments/environment'; 4 | import { Message } from '../models/message'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class MessagesService { 10 | private readonly URL = environment.apiUrl + 'messages'; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | find(params?) { 15 | return this.http.get(this.URL, {params}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chat-client/src/app/services/rooms.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { RoomsService } from './rooms.service'; 4 | 5 | describe('RoomsService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [RoomsService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([RoomsService], (service: RoomsService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /chat-client/src/app/services/rooms.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Room } from '../models/room'; 4 | import { environment } from '../../environments/environment'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class RoomsService { 10 | 11 | constructor(private http: HttpClient) { } 12 | 13 | find(params?) { 14 | return this.http.get(environment.apiUrl + 'rooms/', {params}); 15 | } 16 | 17 | findById(id: string) { 18 | return this.http.get(environment.apiUrl + 'rooms/' + id); 19 | } 20 | 21 | save(item: Room) { 22 | return this.http.post(environment.apiUrl + 'rooms/', item); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /chat-client/src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nest-ionic-examples/02-chat-with-db/349c5ef4e31a5dbaace92f500d0d3733101de6b2/chat-client/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /chat-client/src/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chat-client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /chat-client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | socketIoConfig: { // <1> 8 | url: 'localhost:3000', // <2> 9 | options: {} 10 | }, 11 | apiUrl: 'http://localhost:3000/api/' // <3> 12 | }; 13 | 14 | /* 15 | * For easier debugging in development mode, you can import the following file 16 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 17 | * 18 | * This import should be commented out in production mode because it will have a negative impact 19 | * on performance if an error is thrown. 20 | */ 21 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 22 | -------------------------------------------------------------------------------- /chat-client/src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import "~@ionic/angular/css/core.css"; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import "~@ionic/angular/css/normalize.css"; 17 | @import "~@ionic/angular/css/structure.css"; 18 | @import "~@ionic/angular/css/typography.css"; 19 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import "~@ionic/angular/css/padding.css"; 23 | @import "~@ionic/angular/css/float-elements.css"; 24 | @import "~@ionic/angular/css/text-alignment.css"; 25 | @import "~@ionic/angular/css/text-transformation.css"; 26 | @import "~@ionic/angular/css/flex-utils.css"; 27 | -------------------------------------------------------------------------------- /chat-client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ionic App 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /chat-client/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 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /chat-client/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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | import './zone-flags'; 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /chat-client/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/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /chat-client/src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #3dc2ff; 16 | --ion-color-secondary-rgb: 61, 194, 255; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #36abe0; 20 | --ion-color-secondary-tint: #50c8ff; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #5260ff; 24 | --ion-color-tertiary-rgb: 82, 96, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #4854e0; 28 | --ion-color-tertiary-tint: #6370ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #2dd36f; 32 | --ion-color-success-rgb: 45, 211, 111; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #28ba62; 36 | --ion-color-success-tint: #42d77d; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffc409; 40 | --ion-color-warning-rgb: 255, 196, 9; 41 | --ion-color-warning-contrast: #000000; 42 | --ion-color-warning-contrast-rgb: 0, 0, 0; 43 | --ion-color-warning-shade: #e0ac08; 44 | --ion-color-warning-tint: #ffca22; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #eb445a; 48 | --ion-color-danger-rgb: 235, 68, 90; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #cf3c4f; 52 | --ion-color-danger-tint: #ed576b; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 36, 40; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #92949c; 64 | --ion-color-medium-rgb: 146, 148, 156; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #808289; 68 | --ion-color-medium-tint: #9d9fa6; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 245, 248; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | /* 81 | * Dark Colors 82 | * ------------------------------------------- 83 | */ 84 | 85 | body { 86 | --ion-color-primary: #428cff; 87 | --ion-color-primary-rgb: 66,140,255; 88 | --ion-color-primary-contrast: #ffffff; 89 | --ion-color-primary-contrast-rgb: 255,255,255; 90 | --ion-color-primary-shade: #3a7be0; 91 | --ion-color-primary-tint: #5598ff; 92 | 93 | --ion-color-secondary: #50c8ff; 94 | --ion-color-secondary-rgb: 80,200,255; 95 | --ion-color-secondary-contrast: #ffffff; 96 | --ion-color-secondary-contrast-rgb: 255,255,255; 97 | --ion-color-secondary-shade: #46b0e0; 98 | --ion-color-secondary-tint: #62ceff; 99 | 100 | --ion-color-tertiary: #6a64ff; 101 | --ion-color-tertiary-rgb: 106,100,255; 102 | --ion-color-tertiary-contrast: #ffffff; 103 | --ion-color-tertiary-contrast-rgb: 255,255,255; 104 | --ion-color-tertiary-shade: #5d58e0; 105 | --ion-color-tertiary-tint: #7974ff; 106 | 107 | --ion-color-success: #2fdf75; 108 | --ion-color-success-rgb: 47,223,117; 109 | --ion-color-success-contrast: #000000; 110 | --ion-color-success-contrast-rgb: 0,0,0; 111 | --ion-color-success-shade: #29c467; 112 | --ion-color-success-tint: #44e283; 113 | 114 | --ion-color-warning: #ffd534; 115 | --ion-color-warning-rgb: 255,213,52; 116 | --ion-color-warning-contrast: #000000; 117 | --ion-color-warning-contrast-rgb: 0,0,0; 118 | --ion-color-warning-shade: #e0bb2e; 119 | --ion-color-warning-tint: #ffd948; 120 | 121 | --ion-color-danger: #ff4961; 122 | --ion-color-danger-rgb: 255,73,97; 123 | --ion-color-danger-contrast: #ffffff; 124 | --ion-color-danger-contrast-rgb: 255,255,255; 125 | --ion-color-danger-shade: #e04055; 126 | --ion-color-danger-tint: #ff5b71; 127 | 128 | --ion-color-dark: #f4f5f8; 129 | --ion-color-dark-rgb: 244,245,248; 130 | --ion-color-dark-contrast: #000000; 131 | --ion-color-dark-contrast-rgb: 0,0,0; 132 | --ion-color-dark-shade: #d7d8da; 133 | --ion-color-dark-tint: #f5f6f9; 134 | 135 | --ion-color-medium: #989aa2; 136 | --ion-color-medium-rgb: 152,154,162; 137 | --ion-color-medium-contrast: #000000; 138 | --ion-color-medium-contrast-rgb: 0,0,0; 139 | --ion-color-medium-shade: #86888f; 140 | --ion-color-medium-tint: #a2a4ab; 141 | 142 | --ion-color-light: #222428; 143 | --ion-color-light-rgb: 34,36,40; 144 | --ion-color-light-contrast: #ffffff; 145 | --ion-color-light-contrast-rgb: 255,255,255; 146 | --ion-color-light-shade: #1e2023; 147 | --ion-color-light-tint: #383a3e; 148 | } 149 | 150 | /* 151 | * iOS Dark Theme 152 | * ------------------------------------------- 153 | */ 154 | 155 | .ios body { 156 | --ion-background-color: #000000; 157 | --ion-background-color-rgb: 0,0,0; 158 | 159 | --ion-text-color: #ffffff; 160 | --ion-text-color-rgb: 255,255,255; 161 | 162 | --ion-color-step-50: #0d0d0d; 163 | --ion-color-step-100: #1a1a1a; 164 | --ion-color-step-150: #262626; 165 | --ion-color-step-200: #333333; 166 | --ion-color-step-250: #404040; 167 | --ion-color-step-300: #4d4d4d; 168 | --ion-color-step-350: #595959; 169 | --ion-color-step-400: #666666; 170 | --ion-color-step-450: #737373; 171 | --ion-color-step-500: #808080; 172 | --ion-color-step-550: #8c8c8c; 173 | --ion-color-step-600: #999999; 174 | --ion-color-step-650: #a6a6a6; 175 | --ion-color-step-700: #b3b3b3; 176 | --ion-color-step-750: #bfbfbf; 177 | --ion-color-step-800: #cccccc; 178 | --ion-color-step-850: #d9d9d9; 179 | --ion-color-step-900: #e6e6e6; 180 | --ion-color-step-950: #f2f2f2; 181 | 182 | --ion-item-background: #000000; 183 | 184 | --ion-card-background: #1c1c1d; 185 | } 186 | 187 | .ios ion-modal { 188 | --ion-background-color: var(--ion-color-step-100); 189 | --ion-toolbar-background: var(--ion-color-step-150); 190 | --ion-toolbar-border-color: var(--ion-color-step-250); 191 | } 192 | 193 | 194 | /* 195 | * Material Design Dark Theme 196 | * ------------------------------------------- 197 | */ 198 | 199 | .md body { 200 | --ion-background-color: #121212; 201 | --ion-background-color-rgb: 18,18,18; 202 | 203 | --ion-text-color: #ffffff; 204 | --ion-text-color-rgb: 255,255,255; 205 | 206 | --ion-border-color: #222222; 207 | 208 | --ion-color-step-50: #1e1e1e; 209 | --ion-color-step-100: #2a2a2a; 210 | --ion-color-step-150: #363636; 211 | --ion-color-step-200: #414141; 212 | --ion-color-step-250: #4d4d4d; 213 | --ion-color-step-300: #595959; 214 | --ion-color-step-350: #656565; 215 | --ion-color-step-400: #717171; 216 | --ion-color-step-450: #7d7d7d; 217 | --ion-color-step-500: #898989; 218 | --ion-color-step-550: #949494; 219 | --ion-color-step-600: #a0a0a0; 220 | --ion-color-step-650: #acacac; 221 | --ion-color-step-700: #b8b8b8; 222 | --ion-color-step-750: #c4c4c4; 223 | --ion-color-step-800: #d0d0d0; 224 | --ion-color-step-850: #dbdbdb; 225 | --ion-color-step-900: #e7e7e7; 226 | --ion-color-step-950: #f3f3f3; 227 | 228 | --ion-item-background: #1e1e1e; 229 | 230 | --ion-toolbar-background: #1f1f1f; 231 | 232 | --ion-tab-bar-background: #1f1f1f; 233 | 234 | --ion-card-background: #1e1e1e; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /chat-client/src/zone-flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents Angular change detection from 3 | * running with certain Web Component callbacks 4 | */ 5 | // eslint-disable-next-line no-underscore-dangle 6 | (window as any).__Zone_disable_customElements = true; 7 | -------------------------------------------------------------------------------- /chat-client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /chat-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2020", 14 | "module": "es2020", 15 | "lib": ["es2018", "dom"] 16 | }, 17 | "angularCompilerOptions": { 18 | "enableI18nLegacyMessageIdFormat": false, 19 | "strictInjectionParameters": true, 20 | "strictInputAccessModifiers": true, 21 | "strictTemplates": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chat-client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /chat-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nest-ionic-examples/02-chat-with-db/349c5ef4e31a5dbaace92f500d0d3733101de6b2/chat-demo.gif -------------------------------------------------------------------------------- /highlight.min.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("typescript",function(e){var r={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"};return{aliases:["ts"],k:r,c:[{cN:"meta",b:/^\s*['"]use strict['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+e.IR+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:e.IR},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:["self",e.CLCM,e.CBCM]}]}]}],r:0},{cN:"function",b:"function",e:/[\{;]/,eE:!0,k:r,c:["self",e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:[e.CLCM,e.CBCM],i:/["'\(]/}],i:/%/,r:0},{bK:"constructor",e:/\{/,eE:!0,c:["self",{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:[e.CLCM,e.CBCM],i:/["'\(]/}]},{b:/module\./,k:{built_in:"module"},r:0},{bK:"module",e:/\{/,eE:!0},{bK:"interface",e:/\{/,eE:!0,k:"interface extends"},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("scss",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",i={cN:"variable",b:"(\\$"+t+")\\b"},r={cN:"number",b:"#[0-9A-Fa-f]+"};({cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{eW:!0,eE:!0,c:[r,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"meta",b:"!important"}]}});return{cI:!0,i:"[=/|']",c:[e.CLCM,e.CBCM,{cN:"selector-id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"selector-class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"selector-attr",b:"\\[",e:"\\]",i:"$"},{cN:"selector-tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",r:0},{b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},i,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{b:":",e:";",c:[i,r,e.CSSNM,e.QSM,e.ASM,{cN:"meta",b:"!important"}]},{b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[i,e.QSM,e.ASM,r,e.CSSNM,{b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}}); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple Chat example using Ionic, Nest, Socket.IO and MongoDB 9 | 10 | 438 | 439 | 440 | 441 | 442 | 467 |
468 |
469 |
470 |
471 |
472 | 475 |
476 |
477 |
478 |

In previous tutorial we created a simple chat application, in this tutorial we are going to add support for MongoDb database to store messages, users and rooms.

479 |
480 |
481 |
482 | chat demo 483 |
484 |
485 |
486 |
487 |
488 |

Nest Application

489 |
490 |
491 |

Database Setup

492 |
493 |

In this tutorial we are going to use MongoDb as database however you could use another database with some little tweaks. We are going to explore two ways to add MongoDb locally.

494 |
495 |
496 | 497 | 498 | 501 | 511 | 512 |
499 | 500 | 502 |
503 |

Before continue with this part, in the console go to chat-api folder:

504 |
505 |
506 |
507 |
cd chat-api
508 |
509 |
510 |
513 |
514 |
515 |

Brew Installation

516 |
517 |

The first one is by using brew or linuxbrew, so you could run:

518 |
519 |
520 |
521 |
brew install mongodb
522 |
523 |
524 |
525 |

then you could just start it running

526 |
527 |
528 |
529 |
brew services start mongo
530 |
531 |
532 |
533 |

Then start the mongo shell pointing to the chat database:

534 |
535 |
536 |
537 |
mongo chat
538 |
539 |
540 |
541 |

After you start the mongo-shell you need to add user chat-admin, set password password123 and add the role dbAdmin for chat database. so run next commands in the console:

542 |
543 |
544 |
545 |
db.createUser({user: 'chat-admin', pwd: 'password123', roles: [{role: 'dbAdmin', db: 'chat'}]})
546 |
547 |
548 |
549 |

then you are going to get something like next:

550 |
551 |
552 |
553 |
Successfully added user: {
 554 |         "user" : "chat-admin",
 555 |         "roles" : [
 556 |                 {
 557 |                         "role" : "dbAdmin",
 558 |                         "db" : "chat"
 559 |                 }
 560 |         ]
 561 | }
562 |
563 |
564 |
565 |

then just press combination ctrl + C to stop mongo shell.

566 |
567 |
568 | 569 | 570 | 573 | 583 | 584 |
571 | 572 | 574 |
575 |

If you want to start another instance of mongodb in the same port, for example with docker-compose, then you need to stop this one. Hence, you could run:

576 |
577 |
578 |
579 |
brew services stop mongo
580 |
581 |
582 |
585 |
586 |
587 |
588 |

Docker Compose Setup

589 |
590 |

The second way is to use docker and docker-compose. To do that, we need to create a docker-compose.yaml file and add next code:

591 |
592 |
593 |
594 |
version: '3.0'
 595 | services:
 596 |   mongo:
 597 |     image: mongo
 598 |     ports:
 599 |       - 27017:27017 (1)
 600 |     volumes:
 601 |       - ./docker-compose-files:/docker-entrypoint-initdb.d (2)
 602 |     environment:
 603 |       MONGO_INITDB_DATABASE: chat (3)
604 |
605 |
606 |
607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 |
1whenever we run docker-compose up mongo the database will run and listen on port 27017.
2We create a virtual volume from docker-compose-files folder created in the host machine and share it inside the container as docker-entrypoint-initdb.d. In this folder we will put initialization scripts to start the database with preloaded data.
3We set the default database that will be used by the application
621 |
622 |
623 |

As you can see in the previous file docker-compose needs initialization files to start the database, so we will create file docker-compose-files/initdbs.js which will contain the initialization script. Then add next code to it:

624 |
625 |
626 |
627 |
db.createUser({
 628 |   user: 'chat-admin',
 629 |   pwd: 'password123',
 630 |   roles: [{role: 'dbAdmin', db: 'chat'}]
 631 | }); (1)
632 |
633 |
634 |
635 | 636 | 637 | 638 | 639 | 640 |
1creates a new user chat-admin, sets password password123 and add the role dbAdmin for chat database.
641 |
642 |
643 |

There are several advantages of using docker-compose over local brew installation:

644 |
645 |
646 |
    647 |
  1. 648 |

    We can create a new MongoDB database for every project without affecting others.

    649 |
  2. 650 |
  3. 651 |

    We could destroy the database data just running: docker-compose down -v and restart it running docker-compose up mongo. This is very important for integration testing.

    652 |
  4. 653 |
  5. 654 |

    We can specify NodeJs and MongoDB version we are working, so others just need to run docker-compose up mongo to have the same environment as us.

    655 |
  6. 656 |
657 |
658 |
659 |
660 |
661 |

Nestjs/Mongoose

662 |
663 |

In order to add support for mongodb database, Nest comes with the ready to use @nestjs/mongoose package. This is one of the most mature available so far. Since it’s written in TypeScript, it works pretty well with the Nest framework.

664 |
665 |
666 |

Firstly, we need to install all of the required dependencies:

667 |
668 |
669 |
670 |
npm i -s @nestjs/mongoose mongoose
671 |
672 |
673 |
674 |

Once the installation process is completed, we can import the MongooseModule into the root ApplicationModule.

675 |
676 |
677 |
678 |
import { Module } from '@nestjs/common';
 679 | import { AppController } from './app.controller';
 680 | import { AppService } from './app.service';
 681 | import { MessagesGateway } from './gateways/messages/messages.gateway';
 682 | import { RoomsController } from './controllers/rooms/rooms.controller';
 683 | import { MongooseModule } from "@nestjs/mongoose"; (1)
 684 | import { Message, MessageSchema } from './models/message.model';
 685 | import { Room, RoomSchema } from './models/room.model';
 686 | import { User, UserSchema } from './models/user.model';
 687 | 
 688 | @Module({
 689 |   imports: [
 690 |     MongooseModule.forRoot('mongodb://chat-admin:password123@localhost/chat', {}), (2)
 691 |     MongooseModule.forFeature([
 692 |       {name: Message.name, schema: MessageSchema},
 693 |       {name: Room.name, schema: RoomSchema},
 694 |       {name: User.name, schema: UserSchema}
 695 |     ]), (3)
 696 |   ],
 697 |   controllers: [
 698 |     AppController,
 699 |     RoomsController,
 700 |   ],
 701 |   providers: [AppService, MessagesGateway],
 702 | })
 703 | export class AppModule {
 704 | }
705 |
706 |
707 |
708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 |
1Import MongooseModule from @nestjs/mongoose package
2Add MongooseModule configurations for mongo database connection
3Add models to MongooseModule so they can be injected later in the application components
722 |
723 |
724 |

Now we need to create three models Message, Room, and User.

725 |
726 |
727 |
src/models/message.model.ts
728 |
729 |
import {User} from './user.model';
 730 | import {Room} from './room.model';
 731 | import {ObjectID} from 'bson';
 732 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose";
 733 | import {Types} from "mongoose";
 734 | 
 735 | @Schema()
 736 | export class Message {
 737 | 
 738 |   _id: ObjectID | string;
 739 | 
 740 |   @Prop({required: true})
 741 |   text: string;
 742 | 
 743 |   @Prop({required: true})
 744 |   created: Date;
 745 | 
 746 |   @Prop({required: true, ref: 'User', type: Types.ObjectId})
 747 |   owner: User;
 748 | 
 749 |   @Prop({required: true, ref: 'Room', type: Types.ObjectId})
 750 |   room: Room | string;
 751 | }
 752 | 
 753 | export const MessageSchema = SchemaFactory.createForClass(Message)
754 |
755 |
756 |
757 |
src/models/room.model.ts
758 |
759 |
import {Message} from './message.model';
 760 | import {User} from './user.model';
 761 | import {ObjectID} from 'bson';
 762 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose";
 763 | import {Types} from "mongoose";
 764 | 
 765 | @Schema()
 766 | export class Room {
 767 |   _id: ObjectID | string;
 768 | 
 769 |   @Prop({required: true, maxlength: 20, minlength: 5})
 770 |   name: string;
 771 | 
 772 |   @Prop({type: [{type: Types.ObjectId, ref: 'Message'}]})
 773 |   messages: Message[];
 774 | 
 775 |   @Prop({type: [{type: Types.ObjectId, ref: 'User'}]})
 776 |   connectedUsers: User[];
 777 | }
 778 | 
 779 | export const RoomSchema = SchemaFactory.createForClass(Room)
780 |
781 |
782 |
783 |
src/models/user.model.ts
784 |
785 |
import {Message} from './message.model';
 786 | import {Room} from './room.model';
 787 | import {ObjectID} from 'bson';
 788 | import {Types} from "mongoose";
 789 | import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose";
 790 | 
 791 | @Schema()
 792 | export class User {
 793 |   _id?: ObjectID | string;
 794 | 
 795 |   @Prop({required: true, maxlength: 20, minlength: 5})
 796 |   nickname: string;
 797 | 
 798 |   @Prop({required: true})
 799 |   clientId: string;
 800 | 
 801 |   @Prop({type: [{type: Types.ObjectId, ref: 'Message'}]})
 802 |   messages?: Message[];
 803 | 
 804 |   @Prop({type: [{type: Types.ObjectId, ref: 'Room'}]})
 805 |   joinedRooms?: Room[];
 806 | }
 807 | 
 808 | export const UserSchema = SchemaFactory.createForClass(User)
809 |
810 |
811 |
812 |
813 |

Modify Gateways and Controllers

814 |
815 |

After that we need to modify messages.gateway.ts, so we inject each db model:

816 |
817 |
818 |
819 |
import {OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer} from '@nestjs/websockets';
 820 | import { Socket } from 'socket.io';
 821 | import { Message } from '../../models/message.model';
 822 | import { User } from '../../models/user.model';
 823 | import { Room } from '../../models/room.model';
 824 | import {InjectModel} from "@nestjs/mongoose";
 825 | import {Model} from "mongoose";
 826 | import {Server} from "socket.io";
 827 | 
 828 | @WebSocketGateway({cors: '*:*'})
 829 | export class MessagesGateway implements OnGatewayDisconnect {
 830 | 
 831 |   constructor(@InjectModel(Message.name) private readonly messagesModel: Model<Message>,
 832 |               @InjectModel(Room.name) private readonly roomsModel: Model<Room>,
 833 |               @InjectModel(User.name) private readonly usersModel: Model<User>) { (1)
 834 |   }
 835 | 
 836 |   @WebSocketServer()
 837 |   server: Server;
 838 | 
 839 |   async handleDisconnect(client: Socket) { (2)
 840 |     const user = await this.usersModel.findOne({clientId: client.id});
 841 |     if (user) {
 842 |       this.server.emit('users-changed', {user: user.nickname, event: 'left'});
 843 |       user.clientId = null;
 844 |       await this.usersModel.findByIdAndUpdate(user._id, user);
 845 |     }
 846 |   }
 847 | 
 848 |   @SubscribeMessage('enter-chat-room') (3)
 849 |   async enterChatRoom(client: Socket, data: { nickname: string, roomId: string }) {
 850 |     let user = await this.usersModel.findOne({nickname: data.nickname});
 851 |     if (!user) {
 852 |       user = await this.usersModel.create({nickname: data.nickname, clientId: client.id});
 853 |     } else {
 854 |       user.clientId = client.id;
 855 |       user = await this.usersModel.findByIdAndUpdate(user._id, user, {new: true});
 856 |     }
 857 |     client.join(data.roomId);
 858 |     client.broadcast.to(data.roomId)
 859 |       .emit('users-changed', {user: user.nickname, event: 'joined'}); (3)
 860 |   }
 861 | 
 862 |   @SubscribeMessage('leave-chat-room') (4)
 863 |   async leaveChatRoom(client: Socket, data: { nickname: string, roomId: string }) {
 864 |     const user = await this.usersModel.findOne({nickname: data.nickname});
 865 |     client.broadcast.to(data.roomId).emit('users-changed', {user: user.nickname, event: 'left'}); (3)
 866 |     client.leave(data.roomId);
 867 |   }
 868 | 
 869 |   @SubscribeMessage('add-message') (5)
 870 |   async addMessage(client: Socket, message: Message) {
 871 |     message.owner = await this.usersModel.findOne({clientId: client.id});
 872 |     message.created = new Date();
 873 |     message = await this.messagesModel.create(message);
 874 |     this.server.in(message.room as string).emit('message', message);
 875 |   }
 876 | }
877 |
878 |
879 |
880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 |
1Inject mongoose models for: Message, Room, and User.
2Handles user disconnection, it sends an event that the user is disconnected.
3Handles subscription to enter-chat-room event, which is in charge of adding user to a chat room, if the user doesn’t exist then create a new one.
4Handles subscription to leave-chat-room event, which remove user from the chat room and emits users-changed event to all users of the chat-room
5Handles subscription to add-message event, which is in charge of adding messages coming from users in the chat room
902 |
903 |
904 |

Then we need to modify rooms.controller.ts, so we inject Room model into it:

905 |
906 |
907 |
908 |
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
 909 | import { Room } from '../../models/room.model';
 910 | import {Model} from "mongoose";
 911 | import {InjectModel} from "@nestjs/mongoose";
 912 | 
 913 | @Controller('api/rooms')
 914 | export class RoomsController {
 915 |   constructor(@InjectModel(Room.name) private readonly model: Model<Room>) {} (1)
 916 | 
 917 |   @Get()
 918 |   find(@Query('q') q) { (2)
 919 |     if (q) return this.model.find({name: {$regex: new RegExp(`.*${q}.*`)}});
 920 |     else return this.model.find();
 921 |   }
 922 | 
 923 |   @Get('/:id')
 924 |   findById(@Param('id') id: string) { (3)
 925 |     return this.model.findById(id);
 926 |   }
 927 | 
 928 |   @Post()
 929 |   save(@Body() item: Room) { (4)
 930 |     return item._id
 931 |       ? this.model.findByIdAndUpdate(item._id, item, {new: true})
 932 |       : this.model.create(item);
 933 |   }
 934 | }
935 |
936 |
937 |
938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 |
1Inject Room mongoose model
2Handles GET request for api/rooms. This request could contains a query parameter called q. This could contain a partial room.name value so users can query to the database with values that matches the partial value. If the query parameter is not present then it will return the full list of rooms.
3Handles GET request for api/rooms/:id. Finds and returns the full information of the room matching that id.
4Handles the POST request for api/rooms. If the item contains an _id value, it updates any previous created room. if not, it creates a new room with the passed values. Finally, it returns the saved value to the client.
956 |
957 |
958 |

Finally we need to add messages.controller.ts. To do that you should run:

959 |
960 |
961 |
962 |
nest g co controllers/messages
963 |
964 |
965 |
966 |

Then modify messages.controller.ts with next code:

967 |
968 |
969 |
970 |
import { Controller, Get, Query } from '@nestjs/common';
 971 | import { Message } from '../../models/message.model';
 972 | import {Model} from "mongoose";
 973 | import {InjectModel} from "@nestjs/mongoose";
 974 | 
 975 | @Controller('api/messages')
 976 | export class MessagesController {
 977 |   constructor(@InjectModel(Message.name) private readonly model: Model<Message>) {} (1)
 978 | 
 979 |   @Get()
 980 |   find(@Query('where') where) { (2)
 981 |     where = JSON.parse(where || '{}');
 982 |     return this.model.find(where).populate('owner').exec();
 983 |   }
 984 | }
985 |
986 |
987 |
988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 |
1Inject Message model
2Handles GET request for api/messages. This request could contains a query parameter called where. This could contain any query so users can query to the database with values that matches it, for example {owner: {_id: '123'}}. If the where query parameter is not present then it will return the full list of rooms.
998 |
999 |
1000 | 1001 | 1002 | 1005 | 1016 | 1017 |
1003 | 1004 | 1006 |
1007 |

At the moment queries of controllers does not have any validation so it’s pretty dangerous to use it like that.

1008 |
1009 |
1010 |

Also Gateway and Controllers need an authentication and permission system so only users with needed permissions can access and modify data.

1011 |
1012 |
1013 |

In next tutorials I will add a better handling of it.

1014 |
1015 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |

Ionic Application

1024 |
1025 |
1026 |

In the previous tutorial inside our Ionic chat app we created 2 screens: On the first screen we picked a name and join the chat, on the second screen we show the actual chatroom with messages. In this tutorial we are going to modify the first screen, so after setting the nickname we go to another page where we select the chat room from a list, and then in the last screen we will only show messages for that specified chat room.

1027 |
1028 |
1029 | 1030 | 1031 | 1034 | 1044 | 1045 |
1032 | 1033 | 1035 |
1036 |

Before continue with this part, in the console go to chat-client folder:

1037 |
1038 |
1039 |
1040 |
cd chat-clent
1041 |
1042 |
1043 |
1046 |
1047 |
1048 |

Adding Models

1049 |
1050 |

Before continue it is good idea to add the models to our app, so create next files:

1051 |
1052 |
1053 |
src/app/models/message.ts
1054 |
1055 |
import { User } from './user';
1056 | import { Room } from './room';
1057 | 
1058 | export interface Message {
1059 |   _id?: string;
1060 |   text?: string;
1061 |   owner?: User | string;
1062 |   room?: Room | string;
1063 | }
1064 |
1065 |
1066 |
1067 |
src/app/models/room.ts
1068 |
1069 |
import { Message } from './message';
1070 | import { User } from './user';
1071 | 
1072 | export interface Room {
1073 |   _id?: string;
1074 |   name?: string;
1075 |   messages?: Message[];
1076 |   connectedUsers?: User[];
1077 | }
1078 |
1079 |
1080 |
1081 |
src/app/models/user.ts
1082 |
1083 |
import { Message } from './message';
1084 | import { Room } from './room';
1085 | 
1086 | export interface User {
1087 |   _id?: string;
1088 |   nickname?: string;
1089 |   clientId?: string;
1090 |   messages?: Message[];
1091 |   joinedRooms?: Room[];
1092 | }
1093 |
1094 |
1095 |
1096 |

As you can see those files are only interfaces that have the same attributes as the server entities.

1097 |
1098 |
1099 | 1100 | 1101 | 1104 | 1107 | 1108 |
1102 | 1103 | 1105 | Even though there is a way to put this models in a separate library and share this library between the server and client we are not going to do it in this tutorial. 1106 |
1109 |
1110 |
1111 |
1112 |

Joining a Chatroom

1113 |
1114 |

So now we need to add a way to select the chat room after setting the nickname. To do that we should modify the file src/app/pages/home/home.ts with the next code:

1115 |
1116 |
1117 |
1118 |
import { Component } from '@angular/core';
1119 | import { NavController } from '@ionic/angular';
1120 | 
1121 | @Component({
1122 |   selector: 'app-home',
1123 |   templateUrl: 'home.page.html',
1124 |   styleUrls: ['home.page.scss'],
1125 | })
1126 | export class HomePage {
1127 |   nickname = '';
1128 | 
1129 |   constructor(private navController: NavController) { }
1130 | 
1131 |   joinChat() {
1132 |     sessionStorage.setItem('nickname', this.nickname); (1)
1133 |     this.navController.navigateRoot(`select-room`); (2)
1134 |   }
1135 | }
1136 |
1137 |
1138 |
1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 |
1instead setting the nickname in the url-path we save it into the local storage variable so we can use it later.
2we redirect the user to the select-room page
1149 |
1150 |
1151 |

The template file will be the same, so you can keep the next code:

1152 |
1153 |
1154 |
1155 |
<ion-header>
1156 |   <ion-toolbar>
1157 |     <ion-title>
1158 |       Home
1159 |     </ion-title>
1160 |   </ion-toolbar>
1161 | </ion-header>
1162 | 
1163 | <ion-content padding>
1164 |   <ion-item>
1165 |     <ion-label stacked>Set Nickname</ion-label>
1166 |     <ion-input type="text" [(ngModel)]="nickname" placeholder="Nickname"></ion-input>
1167 |   </ion-item>
1168 |   <ion-button full (click)="joinChat()" [disabled]="!nickname">Join Chat as {{ nickname }}</ion-button>
1169 | </ion-content>
1170 |
1171 |
1172 |
1173 |
1174 |

Building the Room Selection Functionality

1175 |
1176 |

In this page the user will select the chat room he will join, so we need a selection list and a filter box. The first step will be to create the page running next command:

1177 |
1178 |
1179 |
1180 |
ionic g page pages/select-room
1181 |
1182 |
1183 |
1184 |

then we modify select-room.page.html to contain next code:

1185 |
1186 |
1187 |
1188 |
<ion-header>
1189 |   <ion-toolbar>
1190 |     <ion-back-button slot="start" defaultHref="/home"></ion-back-button>
1191 |     <ion-title>Select Room</ion-title>
1192 |   </ion-toolbar>
1193 |   <ion-toolbar primary>
1194 |     <ion-searchbar ngModel (ngModelChange)="searchRoom($event)" autocorrect="off"></ion-searchbar> (1)
1195 |   </ion-toolbar>
1196 | </ion-header>
1197 | 
1198 | <ion-content padding>
1199 |   <ion-list>
1200 |     <ion-item *ngFor="let room of rooms" (click)="joinRoom(room)"> (2)
1201 |       <ion-label>{{room.name}}</ion-label>
1202 |     </ion-item>
1203 |   </ion-list>
1204 | </ion-content>
1205 | 
1206 | <ion-footer>
1207 |   <ion-item>
1208 |     <ion-input type="text" placeholder="Add Room" [(ngModel)]="roomName" name="roomName"></ion-input> (3)
1209 |     <ion-button (click)="addRoom()" [disabled]="!roomName" fill="clear" size="large" slot="end">
1210 |       <ion-icon name="add"></ion-icon>
1211 |     </ion-button>
1212 |   </ion-item>
1213 | </ion-footer>
1214 |
1215 |
1216 |
1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 |
1In the header we show the title and a search-box which executes the method searchRoom whenever user types-in and the debounce time has elapsed.
2The content shows the list of rooms filtered by the search box. Whenever the user clicks on any of the items of the list, it will be redirected to the chat-room page.
3The footer contains a text-box that receive the name of a new chat-room and a plus button which executes the method addRoom.
1231 |
1232 |
1233 |

Before modifying the select-room.page.ts file, it will be needed to add the debounce-decorator-ts package. To do it, run next command at the root directory of chat-client app:

1234 |
1235 |
1236 |
1237 |
npm i -s debounce-decorator-ts
1238 |
1239 |
1240 |
1241 |

then we modify select-room.page.ts to contain next code:

1242 |
1243 |
1244 |
1245 |
import { Component, OnInit } from '@angular/core';
1246 | import { Room } from '../../models/room';
1247 | import { RoomsService } from '../../services/rooms.service';
1248 | import { debounceFn } from 'debounce-decorator-ts';
1249 | import { NavController } from '@ionic/angular';
1250 | 
1251 | @Component({
1252 |   selector: 'app-select-room',
1253 |   templateUrl: './select-room.page.html',
1254 |   styleUrls: ['./select-room.page.scss'],
1255 | })
1256 | export class SelectRoomPage implements OnInit {
1257 | 
1258 |   rooms: Room[];
1259 | 
1260 |   roomName: string;
1261 | 
1262 |   constructor(private roomsService: RoomsService,
1263 |               private navController: NavController) { } (1)
1264 | 
1265 |   ngOnInit() {
1266 |     this.searchRoom(''); (2)
1267 |   }
1268 | 
1269 |   @debounceFn(500)
1270 |   searchRoom(q: string) { (3)
1271 |     const params: any = {};
1272 |     if (q) { params.q = q; }
1273 |     this.roomsService.find(params).subscribe(rooms => this.rooms = rooms);
1274 |   }
1275 | 
1276 |   joinRoom(room: Room) { (4)
1277 |     this.navController.navigateRoot('chat-room/' + room._id);
1278 |   }
1279 | 
1280 |   addRoom() { (5)
1281 |     this.roomsService.save({name: this.roomName}).subscribe(room => {
1282 |       this.roomName = '';
1283 |       this.rooms.push(room);
1284 |     });
1285 |   }
1286 | }
1287 |
1288 |
1289 |
1290 | 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 |
1In the constructor we inject
2In the ngOnInit method we search for all the rooms
3The searchRoom method is in charge of searching for rooms in dependence of the parameter q. This method calls roomsService.find method passing parameter q to it. After receiving the rooms list, it fills a local public array to be used by the html template.
4The joinRoom method navigates to chat-room/:id. That id parameter is later used in the chat-room page.
5The addRoom method calls roomsService.save method which sends the information of the new room to the server. After saving the value, it receives the new value and adds it to the rooms local public variable.
1312 |
1313 |
1314 |
1315 |

Modifying the Chat Functionality

1316 |
1317 |

To receive new chat messages inside the room we have to listen for message socket event which receive the messages from the server.

1318 |
1319 |
1320 |

Whenever we get such a message we simply push the new message to an array of messages. Remember, since now we have a database to save historic data, we are going to load it.

1321 |
1322 |
1323 |

Sending a new message is almost the same as before, we simply emit our event to the server with the right type.

1324 |
1325 |
1326 |

Finally, we also listen to the events of users joining and leaving the room and display a little toast whenever someone comes in or leaves the room. It’s the same logic again, with the socket.on() we can listen to all the events broadcasted from our server!

1327 |
1328 |
1329 |

Go ahead and modify chat-room.ts:

1330 |
1331 |
1332 |
1333 |
import { Component, OnDestroy, OnInit } from '@angular/core';
1334 | import { ActivatedRoute } from '@angular/router';
1335 | import { Socket } from 'ngx-socket-io';
1336 | import { ToastController } from '@ionic/angular';
1337 | import { Subscription } from 'rxjs';
1338 | import { MessagesService } from '../../services/messages.service'; (1)
1339 | import { RoomsService } from '../../services/rooms.service';
1340 | import { Room } from '../../models/room';
1341 | import { Message } from '../../models/message';
1342 | 
1343 | @Component({
1344 |   selector: 'app-chat-room',
1345 |   templateUrl: './chat-room.page.html',
1346 |   styleUrls: ['./chat-room.page.scss'],
1347 | })
1348 | export class ChatRoomPage implements OnInit, OnDestroy {
1349 |   messages: Message[] = [];
1350 |   nickname = '';
1351 |   message = '';
1352 |   room: Room = {};
1353 | 
1354 |   subscription: Subscription;
1355 | 
1356 |   constructor(private route: ActivatedRoute,
1357 |               private socket: Socket,
1358 |               private toastCtrl: ToastController,
1359 |               private messagesService: MessagesService,
1360 |               private roomsService: RoomsService) { } (2)
1361 | 
1362 |   ngOnInit() {
1363 |     this.nickname = sessionStorage.getItem('nickname'); (3)
1364 | 
1365 |     this.subscription = this.route.params.subscribe(params => {
1366 |       const roomId = params.roomId; (4)
1367 |       this.socket.emit('enter-chat-room', {roomId, nickname: this.nickname}); (5)
1368 |       this.roomsService.findById(roomId).subscribe(room => { (6)
1369 |         this.room = room; (7)
1370 |         this.messagesService.find({where: JSON.stringify({room: this.room._id})}).subscribe(messages => { (8)
1371 |           this.messages = messages; (9)
1372 |         });
1373 |       });
1374 |     });
1375 | 
1376 |     this.socket.on('message', message => this.messages.push(message));
1377 | 
1378 |     this.socket.on('users-changed', data => {
1379 |       const user = data.user;
1380 |       if (data['event'] === 'left') {
1381 |         this.showToast('User left: ' + user);
1382 |       } else {
1383 |         this.showToast('User joined: ' + user);
1384 |       }
1385 |     });
1386 | 
1387 |   }
1388 | 
1389 |   ngOnDestroy() { (10)
1390 |     this.subscription.unsubscribe();
1391 |     this.socket.removeAllListeners('message');
1392 |     this.socket.removeAllListeners('users-changed');
1393 |     this.socket.emit('leave-chat-room', {roomId: this.room._id, nickname: this.nickname});
1394 |   }
1395 | 
1396 |   sendMessage() {
1397 |     this.socket.emit('add-message', {text: this.message, room: this.room._id}); (11)
1398 |     this.message = '';
1399 |   }
1400 | 
1401 |   async showToast(msg) {
1402 |     const toast = await this.toastCtrl.create({
1403 |       message: msg,
1404 |       duration: 2000
1405 |     });
1406 |     toast.present();
1407 |   }
1408 | }
1409 |
1410 |
1411 |
1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 |
1We import models and services
2We inject services MessagesService and RoomsService in the constructor
3Instead getting nickname from url, we now get it from sessionStorage.
4We get the roomId from the value coming from the route path param.
5We emit that the user has entered to the chat-room.
6Then we get the full information of the chat-room using roomsService.findById method.
7After receiving the full chat-room info, we set it in a local public variable to be accessible to the html template.
8Then we find all the messages of the chat-room using query: {where: JSON.stringify({room: this.room._id})}
9After getting all the messages of the chat-room, we set them in a local public array to be accessible to the html template.
10In the ngOnDestroy method we need to unsubscribe all the subscriptions, remove listeners for socket.io events, and emit leave-chat-room event so the server can know when a user has left the room. This method is always call whenever the user goes back using the back button.
11We emit the add-message event with the message text and the room id.
1458 |
1459 |
1460 |

And also chat-room.html:

1461 |
1462 |
1463 |
1464 |
<ion-header>
1465 |   <ion-toolbar>
1466 |     <ion-back-button slot="start" defaultHref="/select-room"></ion-back-button> (1)
1467 |     <ion-title>
1468 |       Room: {{room.name}} (2)
1469 |     </ion-title>
1470 |   </ion-toolbar>
1471 | </ion-header>
1472 | 
1473 | <ion-content>
1474 |   <ion-grid>
1475 |     <ion-row *ngFor="let message of messages">
1476 | 
1477 |       <ion-col size="9" *ngIf="message.owner.nickname !== nickname" class="message"
1478 |                [ngClass]="{
1479 |                  'my_message': message.owner.nickname === nickname,
1480 |                  'other_message': message.owner.nickname !== nickname
1481 |                 }"> (3)
1482 |         <span class="user_name">{{ message.owner.nickname }}:</span><br>
1483 |         <span>{{ message.text }}</span>
1484 |         <div class="time">{{message.created | date:'dd.MM hh:MM'}}</div>
1485 |       </ion-col>
1486 | 
1487 |       <ion-col offset="3" size="9" *ngIf="message.owner.nickname === nickname" class="message"
1488 |                [ngClass]="{
1489 |                  'my_message': message.owner.nickname === nickname,
1490 |                  'other_message': message.owner.nickname !== nickname
1491 |                }">
1492 |         <span class="user_name">{{ message.owner.nickname }}:</span><br>
1493 |         <span>{{ message.text }}</span>
1494 |         <div class="time">{{message.created | date:'dd.MM hh:mm'}}</div>
1495 |       </ion-col>
1496 | 
1497 |     </ion-row>
1498 |   </ion-grid>
1499 | 
1500 | </ion-content>
1501 | 
1502 | <ion-footer>
1503 |   <ion-item>
1504 |     <ion-input type="text" placeholder="Message..." [(ngModel)]="message"></ion-input>
1505 |     <ion-button fill="clear" color="primary" slot="end" (click)="sendMessage()" [disabled]="!message" size="large">
1506 |       <ion-icon name="send"></ion-icon>
1507 |     </ion-button>
1508 |   </ion-item>
1509 | </ion-footer>
1510 |
1511 |
1512 |
1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 |
1we have to go back to select-room page instead home page
2Instead of just showing Chat in the header, we now show Room: <room-name>.
3now we compare message.owner.nickname instead comparing message.from
1527 |
1528 |
1529 |

Now launch your app and make sure your backend is up and running!

1530 |
1531 |
1532 |

For testing, you can open a browser and another incognito browser like in my example at the top to chat with yourself.

1533 |
1534 |
1535 |
1536 |
1537 |
1538 |

Conclusion

1539 |
1540 |
1541 |

As you can see adding MongoDB to our project was relatively simple. Furthermore, @nestjs/mongoose give us the huge ability to connect and handle the database without writing too much code.

1542 |
1543 |
1544 |

Now this application looks more real since we can store all the messages, rooms, and users in our database. This leads to the fact that now we can create different chat-rooms with separate conversations.

1545 |
1546 |
1547 |

To finish I just want to point that we need to add some sort of authentication and authorization system. So that, only users with required permissions can access rooms and messages. We will do that in next tutorials.

1548 |
1549 |
1550 |
1551 |
1552 |

Comments

1553 |
1554 |
1555 | 1568 | 1569 |
1570 |
1571 |
1572 | 1573 | 1579 | 1580 | -------------------------------------------------------------------------------- /styles/github.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | --------------------------------------------------------------------------------