├── .angulardoc.json ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── graphcool ├── .gitignore ├── .graphcoolrc ├── graphcool.yml ├── package-lock.json ├── package.json ├── src │ ├── email-password │ │ ├── authenticate.graphql │ │ ├── authenticate.ts │ │ ├── loggedInUser.graphql │ │ ├── loggedInUser.ts │ │ ├── signup.graphql │ │ └── signup.ts │ └── permissions │ │ ├── User.graphql │ │ ├── permitChangeMessage.graphql │ │ ├── permitCreateMessage.graphql │ │ ├── permitReadAndChangeChat.graphql │ │ ├── permitReadMessage.graphql │ │ └── permitSendMessageToChat.graphql └── types.graphql ├── gulpfile.js ├── now └── package.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── apollo-config.module.ts │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── chat │ │ ├── chat-routing.module.ts │ │ ├── chat.module.ts │ │ ├── components │ │ │ ├── chat-add-group │ │ │ │ ├── chat-add-group.component.html │ │ │ │ ├── chat-add-group.component.scss │ │ │ │ ├── chat-add-group.component.spec.ts │ │ │ │ └── chat-add-group.component.ts │ │ │ ├── chat-list │ │ │ │ ├── chat-list.component.html │ │ │ │ ├── chat-list.component.scss │ │ │ │ ├── chat-list.component.spec.ts │ │ │ │ └── chat-list.component.ts │ │ │ ├── chat-message │ │ │ │ ├── chat-message.component.html │ │ │ │ ├── chat-message.component.scss │ │ │ │ ├── chat-message.component.spec.ts │ │ │ │ └── chat-message.component.ts │ │ │ ├── chat-tab │ │ │ │ └── chat-tab.component.ts │ │ │ ├── chat-users │ │ │ │ ├── chat-users.component.html │ │ │ │ ├── chat-users.component.scss │ │ │ │ ├── chat-users.component.spec.ts │ │ │ │ └── chat-users.component.ts │ │ │ └── chat-window │ │ │ │ ├── chat-window.component.html │ │ │ │ ├── chat-window.component.scss │ │ │ │ ├── chat-window.component.spec.ts │ │ │ │ ├── chat-window.component.ts │ │ │ │ └── chat-window.resolver.ts │ │ ├── models │ │ │ ├── chat.model.ts │ │ │ └── message.model.ts │ │ └── services │ │ │ ├── chat.graphql.ts │ │ │ ├── chat.service.ts │ │ │ ├── message.graphql.ts │ │ │ └── message.service.ts │ ├── core │ │ ├── components │ │ │ └── not-found │ │ │ │ ├── not-found.component.spec.ts │ │ │ │ └── not-found.component.ts │ │ ├── core.module.spec.ts │ │ ├── core.module.ts │ │ ├── models │ │ │ ├── file.model.ts │ │ │ └── user.model.ts │ │ ├── providers │ │ │ └── graphcool-config.provider.ts │ │ ├── services │ │ │ ├── app-config.service.ts │ │ │ ├── auth.graphql.ts │ │ │ ├── auth.service.ts │ │ │ ├── base.service.ts │ │ │ ├── error.service.ts │ │ │ ├── file.graphql.ts │ │ │ ├── file.service.ts │ │ │ ├── user.graphql.ts │ │ │ └── user.service.ts │ │ └── strategy │ │ │ ├── authenticated-preloading.strategy.ts │ │ │ └── selective-preloading.strategy.ts │ ├── dashboard │ │ ├── components │ │ │ ├── dashboard-header │ │ │ │ ├── dashboard-header.component.html │ │ │ │ ├── dashboard-header.component.scss │ │ │ │ ├── dashboard-header.component.spec.ts │ │ │ │ └── dashboard-header.component.ts │ │ │ ├── dashboard-home │ │ │ │ ├── dashboard-home.component.html │ │ │ │ ├── dashboard-home.component.scss │ │ │ │ ├── dashboard-home.component.spec.ts │ │ │ │ └── dashboard-home.component.ts │ │ │ ├── dashboard-permission-denied │ │ │ │ ├── dashboard-permission-denied.component.spec.ts │ │ │ │ └── dashboard-permission-denied.component.ts │ │ │ └── dashboard-resources │ │ │ │ ├── dashboard-resources.component.spec.ts │ │ │ │ └── dashboard-resources.component.ts │ │ ├── dashboard-routing.module.ts │ │ └── dashboard.module.ts │ ├── login │ │ ├── auth.guard.ts │ │ ├── auto-login.guard.ts │ │ ├── components │ │ │ └── login │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ ├── login-routing.module.ts │ │ └── login.module.ts │ ├── shared │ │ ├── components │ │ │ ├── avatar │ │ │ │ ├── avatar.component.scss │ │ │ │ └── avatar.component.ts │ │ │ ├── base.component.ts │ │ │ ├── dialog-confirm │ │ │ │ ├── dialog-confirm-data.interface.ts │ │ │ │ ├── dialog-confirm.component.spec.ts │ │ │ │ └── dialog-confirm.component.ts │ │ │ ├── image-preview │ │ │ │ ├── image-preview.component.html │ │ │ │ ├── image-preview.component.spec.ts │ │ │ │ └── image-preview.component.ts │ │ │ ├── no-record │ │ │ │ ├── no-record.component.scss │ │ │ │ └── no-record.component.ts │ │ │ └── warning │ │ │ │ ├── warning.component.spec.ts │ │ │ │ └── warning.component.ts │ │ ├── pipes │ │ │ ├── from-now.pipe.spec.ts │ │ │ ├── from-now.pipe.ts │ │ │ ├── read-file.pipe.spec.ts │ │ │ └── read-file.pipe.ts │ │ ├── shared.module.spec.ts │ │ └── shared.module.ts │ ├── storage-keys.ts │ └── user │ │ ├── components │ │ └── user-profile │ │ │ ├── user-profile.component.html │ │ │ ├── user-profile.component.scss │ │ │ ├── user-profile.component.spec.ts │ │ │ └── user-profile.component.ts │ │ ├── user-routing.module.ts │ │ └── user.module.ts ├── assets │ ├── .gitkeep │ └── images │ │ ├── group-no-photo.png │ │ └── user-no-photo.png ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── scss │ └── _variables.scss ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── static └── capa-oficial-curso.png ├── tsconfig.json └── tslint.json /.angulardoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "repoId": "03ff9c63-e673-4824-b208-7fe771d4bff1", 3 | "lastSync": 0 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Plínio Naves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Graphcool Chat 2 | 3 | Este é o repositório do projeto desenvolvido no [Curso completo de Angular 6 (+Apollo, GraphQL e Graphcool) 4 | ](https://www.udemy.com/curso-completo-de-angular-apollo-e-graphql/?couponCode=ANGULARGITHUB) disponível na Udemy.com. 5 | 6 | ![Curso completo de Angular 6 (+Apollo, GraphQL e Graphcool)](static/capa-oficial-curso.png) 7 | 8 | A aplicação é um Chat Realtime desenvolvido com as seguintes tecnologias e recursos: 9 | 10 | * Client Side 11 | * Angular (6+) 12 | * RxJS 13 | * Apollo Client 14 | * Apollo Angular 15 | * Apollo Cache InMemory 16 | * Apollo Cache Persist 17 | 18 | * Server Side 19 | * Graphcool (BaaS) 20 | * API GraphQL (Queries, Mutations e Realtime Subscriptions) 21 | * Regras de permissão de acesso 22 | * Upload de arquivos 23 | 24 | 25 | ## Gostaria de testar? 26 | 27 | O Chat está disponível como uma Web App, acesse o link abaixo para testar: 28 | 29 | * [Angular Graphcool Chat](https://angular-graphcool-chat.now.sh) 30 | 31 | ## Conteúdo do curso 32 | 33 | O curso trata sobre uma série de assuntos, entre eles: 34 | 35 | * Instalação, configuração e utilização do Apollo Angular 36 | * Modelagem de dados com GraphQL Types 37 | * Autenticação com JSON Web Tokens (JWT) no Angular (e no Graphcool) 38 | * Lista de permissões 39 | * AuthState (com RxJS ReplaySubject) e Login Automático 40 | * Template Driven e Reactive Forms 41 | * Roteamento e Guardas de Rotas 42 | * Modularização, Lazy Loading e estratégias de Preloading 43 | * Comunicação entre Components com Input e Output Properties 44 | * Content Projection 45 | * Queries 46 | * Mutations 47 | * Realtime Subscriptions 48 | * Interceptação de requisições HTTP e WebSocket 49 | * Chats one-to-one e grupos 50 | * Otimização no Apollo com watchQuery, acesso direto ao cache e Optimistic UI 51 | * Upload de imagens 52 | * Build de produção + deploy 53 | * e muito mais! 54 | 55 | Veja a [grade completa na página do curso](https://www.udemy.com/curso-completo-de-angular-apollo-e-graphql/?couponCode=ANGULARGITHUB). 56 | 57 | ## Teste localmente 58 | 59 | Se quiser testar o projeto localmente basta seguir estes passos: 60 | 61 | 1. Clone o repositório 62 | ```bash 63 | git clone git@github.com:plinionaves/angular-graphcool-chat.git 64 | ``` 65 | 66 | 2. Acesse o diretório criado para o projeto 67 | ```bash 68 | cd angular-graphcool-chat 69 | ``` 70 | 71 | 3. Instale as dependências: 72 | ```bash 73 | npm install 74 | ``` 75 | 76 | 4. Execute 77 | ```bash 78 | ng serve -o 79 | ``` 80 | *É necessário ter o [Angular CLI](https://github.com/angular/angular-cli) instalado para rodar o comando acima* 81 | 82 | ## Contato 83 | 84 | Desenvolvido por: [Plínio Naves](https://www.udemy.com/user/plinio-naves/) 85 | 86 | * Email: [pliniopjn@hotmail.com](mailto:pliniopjn@hotmail.com) 87 | * Twitter: [@plinionaves](https://twitter.com/plinionaves) 88 | * Github: [github.com/plinionaves](https://github.com/plinionaves) 89 | * Linkedin: [linkedin.com/in/plinionaves/](https://www.linkedin.com/in/plinionaves/) 90 | 91 | Participe do nosso grupo no Facebook: [Cursos Plínio Naves](https://www.facebook.com/groups/200267383740594) 92 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-graphcool-chat": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/angular-graphcool-chat", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | { 31 | "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css" 32 | }, 33 | "src/styles.scss" 34 | ], 35 | "stylePreprocessorOptions": { 36 | "includePaths": [ 37 | "src/scss" 38 | ] 39 | }, 40 | "scripts": [] 41 | }, 42 | "configurations": { 43 | "production": { 44 | "fileReplacements": [ 45 | { 46 | "replace": "src/environments/environment.ts", 47 | "with": "src/environments/environment.prod.ts" 48 | } 49 | ], 50 | "optimization": true, 51 | "outputHashing": "all", 52 | "sourceMap": false, 53 | "extractCss": true, 54 | "namedChunks": false, 55 | "aot": true, 56 | "extractLicenses": true, 57 | "vendorChunk": false, 58 | "buildOptimizer": true 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "angular-graphcool-chat:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "angular-graphcool-chat:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "angular-graphcool-chat:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "styles": [ 87 | { 88 | "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css" 89 | }, 90 | "styles.scss" 91 | ], 92 | "scripts": [], 93 | "assets": [ 94 | "src/favicon.ico", 95 | "src/assets" 96 | ] 97 | } 98 | }, 99 | "lint": { 100 | "builder": "@angular-devkit/build-angular:tslint", 101 | "options": { 102 | "tsConfig": [ 103 | "src/tsconfig.app.json", 104 | "src/tsconfig.spec.json" 105 | ], 106 | "exclude": [ 107 | "**/node_modules/**" 108 | ] 109 | } 110 | } 111 | } 112 | }, 113 | "angular-graphcool-chat-e2e": { 114 | "root": "e2e/", 115 | "projectType": "application", 116 | "architect": { 117 | "e2e": { 118 | "builder": "@angular-devkit/build-angular:protractor", 119 | "options": { 120 | "protractorConfig": "e2e/protractor.conf.js", 121 | "devServerTarget": "angular-graphcool-chat:serve" 122 | } 123 | }, 124 | "lint": { 125 | "builder": "@angular-devkit/build-angular:tslint", 126 | "options": { 127 | "tsConfig": "e2e/tsconfig.e2e.json", 128 | "exclude": [ 129 | "**/node_modules/**" 130 | ] 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "defaultProject": "angular-graphcool-chat" 137 | } 138 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /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.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /graphcool/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | project-info/ 3 | -------------------------------------------------------------------------------- /graphcool/.graphcoolrc: -------------------------------------------------------------------------------- 1 | targets: 2 | prod: shared-eu-west-1/cjgzdlduy0d100166nmfdz1qr 3 | default: prod 4 | -------------------------------------------------------------------------------- /graphcool/graphcool.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Graphcool! 2 | # 3 | # This file is the main config file for your Graphcool Service. 4 | # It's very minimal at this point and uses default values. 5 | # We've included a hello world function here. 6 | # Just run `graphcool deploy` to have the first running Graphcool Service. 7 | # 8 | # Check out some examples: 9 | # https://github.com/graphcool/framework/tree/master/examples 10 | # 11 | # Here are the reference docs of this definition format: 12 | # https://www.graph.cool/docs/reference/service-definition/graphcool.yml-foatho8aip 13 | # 14 | # Happy Coding! 15 | 16 | 17 | # In the types.graphql you define your data schema 18 | types: ./types.graphql 19 | 20 | 21 | functions: 22 | 23 | # added by email-password template: (please uncomment) 24 | 25 | signup: 26 | type: resolver 27 | schema: src/email-password/signup.graphql 28 | handler: 29 | code: src/email-password/signup.ts 30 | 31 | authenticate: 32 | type: resolver 33 | schema: src/email-password/authenticate.graphql 34 | handler: 35 | code: src/email-password/authenticate.ts 36 | 37 | loggedInUser: 38 | type: resolver 39 | schema: src/email-password/loggedInUser.graphql 40 | handler: 41 | code: src/email-password/loggedInUser.ts 42 | 43 | 44 | # Model/Relation permissions are used to limit the API access 45 | # To take the burden of thinking about those while development, we 46 | # preconfigured the wildcard ("*") permission that allows everything 47 | # Read more here: 48 | # https://www.graph.cool/docs/reference/auth/authorization/overview-iegoo0heez 49 | permissions: 50 | 51 | # User 52 | - operation: User.read 53 | fields: [id, password] 54 | - operation: User.create 55 | fields: [name, email, password] 56 | 57 | - operation: User.read 58 | authenticated: true 59 | query: src/permissions/User.graphql 60 | 61 | - operation: User.read 62 | fields: [name, email, createdAt, updatedAt, photo] 63 | authenticated: true 64 | 65 | - operation: User.update 66 | authenticated: true 67 | query: src/permissions/User.graphql 68 | - operation: User.update 69 | fields: [photo] 70 | authenticated: true 71 | - operation: User.delete 72 | authenticated: true 73 | query: src/permissions/User.graphql 74 | 75 | # Chat 76 | - operation: Chat.read 77 | authenticated: true 78 | query: src/permissions/permitReadAndChangeChat.graphql 79 | - operation: Chat.create 80 | authenticated: true 81 | - operation: Chat.update 82 | authenticated: true 83 | query: src/permissions/permitReadAndChangeChat.graphql 84 | - operation: Chat.delete 85 | authenticated: true 86 | query: src/permissions/permitReadAndChangeChat.graphql 87 | 88 | # Message 89 | - operation: Message.read 90 | authenticated: true 91 | query: src/permissions/permitReadMessage.graphql 92 | - operation: Message.create 93 | authenticated: true 94 | query: src/permissions/permitCreateMessage.graphql 95 | - operation: Message.update 96 | authenticated: true 97 | query: src/permissions/permitChangeMessage.graphql 98 | - operation: Message.delete 99 | authenticated: true 100 | query: src/permissions/permitChangeMessage.graphql 101 | 102 | # File 103 | - operation: File.read 104 | authenticated: true 105 | - operation: File.create 106 | authenticated: true 107 | - operation: File.update 108 | authenticated: true 109 | - operation: File.delete 110 | authenticated: true 111 | 112 | # Relations 113 | - operation: UsersOnChats.connect 114 | authenticated: true 115 | - operation: UsersOnChats.disconnect 116 | authenticated: true 117 | 118 | - operation: MessagesOnChat.connect 119 | authenticated: true 120 | query: src/permissions/permitSendMessageToChat.graphql 121 | 122 | - operation: MessagesOnUser.connect 123 | authenticated: true 124 | 125 | - operation: UserPhoto.connect 126 | authenticated: true 127 | - operation: UserPhoto.disconnect 128 | authenticated: true 129 | 130 | - operation: ChatPhoto.connect 131 | authenticated: true 132 | - operation: ChatPhoto.disconnect 133 | authenticated: true 134 | 135 | # read, create, update, delete 136 | # connect, disconnect 137 | 138 | 139 | # Your root tokens used for functions to get full access to the API 140 | # Read more here: 141 | # https://www.graph.cool/docs/reference/auth/authentication/authentication-tokens-eip7ahqu5o 142 | # rootTokens: 143 | # - mytoken 144 | 145 | -------------------------------------------------------------------------------- /graphcool/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphcool", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/bcryptjs": { 8 | "version": "2.4.1", 9 | "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.1.tgz", 10 | "integrity": "sha512-CVJ8ExtzUQJzLJbEk/lWrHD3MTvstTodjWidcH23gCii5WSD0z1TPSLqSdtbn5eCDw+DxfKgoUALi+loe8ftXA==" 11 | }, 12 | "@types/graphql": { 13 | "version": "0.12.6", 14 | "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-0.12.6.tgz", 15 | "integrity": "sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ==" 16 | }, 17 | "@types/validator": { 18 | "version": "6.3.0", 19 | "resolved": "https://registry.npmjs.org/@types/validator/-/validator-6.3.0.tgz", 20 | "integrity": "sha512-fUc+9BEr1WWW8wVRl5I/pyRDGZNe9nH06kOzlGH3eN7N8VVPW/zSrE0Vdca6G2sRCVmIN/XuOenbmldAAyTYaw==" 21 | }, 22 | "apollo-fetch": { 23 | "version": "0.6.0", 24 | "resolved": "https://registry.npmjs.org/apollo-fetch/-/apollo-fetch-0.6.0.tgz", 25 | "integrity": "sha1-qumyjBF680SwkeyLpNGlqgR03F0=", 26 | "requires": { 27 | "isomorphic-fetch": "^2.2.1" 28 | } 29 | }, 30 | "apollo-link": { 31 | "version": "0.7.0", 32 | "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-0.7.0.tgz", 33 | "integrity": "sha512-LPwygaqW57k7D5rKEdd5xZcLL7SY7MbUkvDYXfU/+el6Iq46AXfxJICXrU6UVydngEBD1DYm0t8CT4JRk0MR7g==", 34 | "requires": { 35 | "apollo-utilities": "^0.2.0-beta.0", 36 | "graphql": "^0.11.3", 37 | "zen-observable-ts": "^0.5.0" 38 | } 39 | }, 40 | "apollo-link-http": { 41 | "version": "0.7.0", 42 | "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-0.7.0.tgz", 43 | "integrity": "sha512-BCYxduD5oHfsiWz15pcjooyAsP4E6gmL1BH4z4ikwU6I++QhbIBt5zH+ezUQcOKcozNY7+ZfcA4oaRud9FbkbA==", 44 | "requires": { 45 | "apollo-fetch": "^0.6.0", 46 | "graphql": "^0.11.0" 47 | } 48 | }, 49 | "apollo-utilities": { 50 | "version": "0.2.0-rc.3", 51 | "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-0.2.0-rc.3.tgz", 52 | "integrity": "sha512-UM5ok/DUKSgh/3T302hoPqCAhqfXdvBaQKOQJb0QUuX3qu2qVKzvwFsv/C3zWYySeCJTP9EoV2LtooIJOhSL4g==" 53 | }, 54 | "bcryptjs": { 55 | "version": "2.4.3", 56 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 57 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 58 | }, 59 | "cross-fetch": { 60 | "version": "2.0.0", 61 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.0.0.tgz", 62 | "integrity": "sha512-gnx0GnDyW73iDq6DpqceL8i4GGn55PPKDzNwZkopJ3mKPcfJ0BUIXBsnYfJBVw+jFDB+hzIp2ELNRdqoxN6M3w==", 63 | "requires": { 64 | "node-fetch": "2.0.0", 65 | "whatwg-fetch": "2.0.3" 66 | }, 67 | "dependencies": { 68 | "node-fetch": { 69 | "version": "2.0.0", 70 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz", 71 | "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y=" 72 | }, 73 | "whatwg-fetch": { 74 | "version": "2.0.3", 75 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", 76 | "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" 77 | } 78 | } 79 | }, 80 | "deprecated-decorator": { 81 | "version": "0.1.6", 82 | "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", 83 | "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" 84 | }, 85 | "encoding": { 86 | "version": "0.1.12", 87 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 88 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 89 | "requires": { 90 | "iconv-lite": "~0.4.13" 91 | } 92 | }, 93 | "graphcool-lib": { 94 | "version": "0.1.4", 95 | "resolved": "https://registry.npmjs.org/graphcool-lib/-/graphcool-lib-0.1.4.tgz", 96 | "integrity": "sha512-qUmcS+nQLPR3gTOvfQbHAdwKkwBJlUIm1SNb+5lcPkh3iIPXSISHJcpt7kECNB2s75O8shKYGMXJRd3F5S2Aaw==", 97 | "requires": { 98 | "apollo-link": "^0.7.0", 99 | "apollo-link-http": "^0.7.0", 100 | "graphql": "^0.11.2", 101 | "graphql-request": "^1.3.4", 102 | "graphql-tools": "^2.4.0", 103 | "node-fetch": "^1.7.3", 104 | "source-map-support": "^0.4.17" 105 | } 106 | }, 107 | "graphql": { 108 | "version": "0.11.7", 109 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.11.7.tgz", 110 | "integrity": "sha512-x7uDjyz8Jx+QPbpCFCMQ8lltnQa4p4vSYHx6ADe8rVYRTdsyhCJbvSty5DAsLVmU6cGakl+r8HQYolKHxk/tiw==", 111 | "requires": { 112 | "iterall": "1.1.3" 113 | } 114 | }, 115 | "graphql-request": { 116 | "version": "1.6.0", 117 | "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.6.0.tgz", 118 | "integrity": "sha512-qqAPLZuaGlwZDsMQ2FfgEyZMcXFMsPPDl6bQQlmwP/xCnk1TqxkE1S644LsHTXAHYPvmRWsIimfdcnys5+o+fQ==", 119 | "requires": { 120 | "cross-fetch": "2.0.0" 121 | } 122 | }, 123 | "graphql-tools": { 124 | "version": "2.24.0", 125 | "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-2.24.0.tgz", 126 | "integrity": "sha512-Mz9I7jyizrd+RafC/5EogJKTVzBbIddDCrW0sP5QLmsVVM3ujfhqVYu2lEXOaJW8Sy18f3ZICHirmKcn6oMAcA==", 127 | "requires": { 128 | "apollo-link": "^1.2.1", 129 | "apollo-utilities": "^1.0.1", 130 | "deprecated-decorator": "^0.1.6", 131 | "iterall": "^1.1.3", 132 | "uuid": "^3.1.0" 133 | }, 134 | "dependencies": { 135 | "apollo-link": { 136 | "version": "1.2.2", 137 | "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.2.tgz", 138 | "integrity": "sha512-Uk/BC09dm61DZRDSu52nGq0nFhq7mcBPTjy5EEH1eunJndtCaNXQhQz/BjkI2NdrfGI+B+i5he6YSoRBhYizdw==", 139 | "requires": { 140 | "@types/graphql": "0.12.6", 141 | "apollo-utilities": "^1.0.0", 142 | "zen-observable-ts": "^0.8.9" 143 | } 144 | }, 145 | "apollo-utilities": { 146 | "version": "1.0.12", 147 | "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.0.12.tgz", 148 | "integrity": "sha512-3mSen+NLouRwhmzCSHbMICfLBa6J+QJOc+M8zzLyo10jAYsOK+A2VgR63q4mcQJmAp8LumC5VAyah1zw6enMcg==" 149 | }, 150 | "zen-observable-ts": { 151 | "version": "0.8.9", 152 | "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.9.tgz", 153 | "integrity": "sha512-KJz2O8FxbAdAU5CSc8qZ1K2WYEJb1HxS6XDRF+hOJ1rOYcg6eTMmS9xYHCXzqZZzKw6BbXWyF4UpwSsBQnHJeA==", 154 | "requires": { 155 | "zen-observable": "^0.8.0" 156 | } 157 | } 158 | } 159 | }, 160 | "iconv-lite": { 161 | "version": "0.4.23", 162 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 163 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 164 | "requires": { 165 | "safer-buffer": ">= 2.1.2 < 3" 166 | } 167 | }, 168 | "is-stream": { 169 | "version": "1.1.0", 170 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 171 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 172 | }, 173 | "isomorphic-fetch": { 174 | "version": "2.2.1", 175 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 176 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 177 | "requires": { 178 | "node-fetch": "^1.0.1", 179 | "whatwg-fetch": ">=0.10.0" 180 | } 181 | }, 182 | "iterall": { 183 | "version": "1.1.3", 184 | "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.3.tgz", 185 | "integrity": "sha512-Cu/kb+4HiNSejAPhSaN1VukdNTTi/r4/e+yykqjlG/IW+1gZH5b4+Bq3whDX4tvbYugta3r8KTMUiqT3fIGxuQ==" 186 | }, 187 | "node-fetch": { 188 | "version": "1.7.3", 189 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 190 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 191 | "requires": { 192 | "encoding": "^0.1.11", 193 | "is-stream": "^1.0.1" 194 | } 195 | }, 196 | "safer-buffer": { 197 | "version": "2.1.2", 198 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 199 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 200 | }, 201 | "source-map": { 202 | "version": "0.5.7", 203 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 204 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 205 | }, 206 | "source-map-support": { 207 | "version": "0.4.18", 208 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", 209 | "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", 210 | "requires": { 211 | "source-map": "^0.5.6" 212 | } 213 | }, 214 | "uuid": { 215 | "version": "3.2.1", 216 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", 217 | "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" 218 | }, 219 | "validator": { 220 | "version": "9.4.1", 221 | "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", 222 | "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" 223 | }, 224 | "whatwg-fetch": { 225 | "version": "2.0.4", 226 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", 227 | "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" 228 | }, 229 | "zen-observable": { 230 | "version": "0.8.8", 231 | "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.8.tgz", 232 | "integrity": "sha512-HnhhyNnwTFzS48nihkCZIJGsWGFcYUz+XPDlPK5W84Ifji8SksC6m7sQWOf8zdCGhzQ4tDYuMYGu5B0N1dXTtg==" 233 | }, 234 | "zen-observable-ts": { 235 | "version": "0.5.0", 236 | "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.5.0.tgz", 237 | "integrity": "sha512-8soRu9VE2HYkAAKcDegToWzu8snITcZjujnE4SXc+7IczbHXRCFkd4Cj4DgYq88nU6SwTU5lkea7KBGNa/CaFw==" 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /graphcool/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphcool", 3 | "version": "1.0.0", 4 | "description": "My Graphcool Service", 5 | "dependencies": { 6 | "@types/bcryptjs": "^2.4.1", 7 | "@types/validator": "^6.3.0", 8 | "bcryptjs": "^2.4.3", 9 | "graphcool-lib": "^0.1.0", 10 | "graphql-request": "^1.4.0", 11 | "validator": "^9.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphcool/src/email-password/authenticate.graphql: -------------------------------------------------------------------------------- 1 | type AuthenticateUserPayload { 2 | id: ID! 3 | token: String! 4 | } 5 | 6 | extend type Mutation { 7 | authenticateUser(email: String!, password: String!): AuthenticateUserPayload 8 | } 9 | -------------------------------------------------------------------------------- /graphcool/src/email-password/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { default as Graphcool, fromEvent, FunctionEvent } from 'graphcool-lib'; 2 | import { GraphQLClient } from 'graphql-request'; 3 | import * as bcrypt from 'bcryptjs'; 4 | 5 | interface User { 6 | id: string; 7 | password: string; 8 | } 9 | 10 | interface EventData { 11 | email: string; 12 | password: string; 13 | } 14 | 15 | const SALT_ROUNDS = 10; 16 | 17 | export default async (event: FunctionEvent) => { 18 | console.log(event); 19 | 20 | try { 21 | const graphcool: Graphcool = fromEvent(event); 22 | const api: GraphQLClient = graphcool.api('simple/v1'); 23 | 24 | const { email, password } = event.data; 25 | 26 | // get user by email 27 | const user: User = await getUserByEmail(api, email) 28 | .then(r => r.User); 29 | 30 | // no user with this email 31 | if (!user) { 32 | return { error: 'Invalid credentials!' }; 33 | } 34 | 35 | // check password 36 | const passwordIsCorrect = await bcrypt.compare(password, user.password); 37 | if (!passwordIsCorrect) { 38 | return { error: 'Invalid credentials!' }; 39 | } 40 | 41 | // generate node token for existing User node 42 | const token = await graphcool.generateNodeToken(user.id, 'User'); 43 | 44 | return { data: { id: user.id, token} }; 45 | 46 | } catch (e) { 47 | console.log(e); 48 | return { error: 'An unexpected error occured during authentication.' }; 49 | } 50 | }; 51 | 52 | async function getUserByEmail(api: GraphQLClient, email: string): Promise<{ User }> { 53 | const query = ` 54 | query getUserByEmail($email: String!) { 55 | User(email: $email) { 56 | id 57 | password 58 | } 59 | } 60 | `; 61 | 62 | const variables = { 63 | email, 64 | }; 65 | 66 | return api.request<{ User }>(query, variables); 67 | } 68 | -------------------------------------------------------------------------------- /graphcool/src/email-password/loggedInUser.graphql: -------------------------------------------------------------------------------- 1 | type LoggedInUserPayload { 2 | id: ID! 3 | } 4 | 5 | extend type Query { 6 | # return user data if request contains valid authentication token 7 | loggedInUser: LoggedInUserPayload 8 | } 9 | -------------------------------------------------------------------------------- /graphcool/src/email-password/loggedInUser.ts: -------------------------------------------------------------------------------- 1 | import { default as Graphcool, fromEvent, FunctionEvent } from 'graphcool-lib'; 2 | import { GraphQLClient } from 'graphql-request'; 3 | 4 | interface User { 5 | id: string; 6 | } 7 | 8 | export default async (event: FunctionEvent<{}>) => { 9 | console.log(event); 10 | 11 | try { 12 | // no logged in user 13 | if (!event.context.auth || !event.context.auth.nodeId) { 14 | return { data: null }; 15 | } 16 | 17 | const userId = event.context.auth.nodeId; 18 | const graphcool: Graphcool = fromEvent<{}>(event); 19 | const api: GraphQLClient = graphcool.api('simple/v1'); 20 | 21 | // get user by id 22 | const user: User = await getUser(api, userId) 23 | .then(r => r.User); 24 | 25 | // no logged in user 26 | if (!user || !user.id) { 27 | return { data: null }; 28 | } 29 | 30 | return { data: { id: user.id } }; 31 | 32 | } catch (e) { 33 | console.log(e); 34 | return { error: 'An unexpected error occured during authentication.' }; 35 | } 36 | }; 37 | 38 | async function getUser(api: GraphQLClient, id: string): Promise<{ User }> { 39 | const query = ` 40 | query getUser($id: ID!) { 41 | User(id: $id) { 42 | id 43 | } 44 | } 45 | `; 46 | 47 | const variables = { 48 | id, 49 | }; 50 | 51 | return api.request<{ User }>(query, variables); 52 | } 53 | -------------------------------------------------------------------------------- /graphcool/src/email-password/signup.graphql: -------------------------------------------------------------------------------- 1 | type SignupUserPayload { 2 | id: ID! 3 | token: String! 4 | } 5 | 6 | extend type Mutation { 7 | signupUser(name: String!, email: String!, password: String!): SignupUserPayload 8 | } 9 | -------------------------------------------------------------------------------- /graphcool/src/email-password/signup.ts: -------------------------------------------------------------------------------- 1 | import { default as Graphcool, fromEvent, FunctionEvent } from 'graphcool-lib'; 2 | import { GraphQLClient } from 'graphql-request'; 3 | import * as bcrypt from 'bcryptjs'; 4 | import * as validator from 'validator'; 5 | 6 | interface User { 7 | id: string; 8 | } 9 | 10 | interface EventData { 11 | name: string; 12 | email: string; 13 | password: string; 14 | } 15 | 16 | const SALT_ROUNDS = 10; 17 | 18 | export default async (event: FunctionEvent) => { 19 | console.log(event); 20 | 21 | try { 22 | const graphcool: Graphcool = fromEvent(event); 23 | const api: GraphQLClient = graphcool.api('simple/v1'); 24 | 25 | const { name, email, password } = event.data; 26 | 27 | if (!validator.isEmail(email)) { 28 | return { error: 'Not a valid email' }; 29 | } 30 | 31 | // check if user exists already 32 | const userExists: boolean = await getUser(api, email) 33 | .then(r => r.User !== null); 34 | if (userExists) { 35 | return { error: 'Email already in use' }; 36 | } 37 | 38 | // create password hash 39 | const salt = bcrypt.genSaltSync(SALT_ROUNDS); 40 | const hash = await bcrypt.hash(password, salt); 41 | 42 | // create new user 43 | const userId = await createGraphcoolUser(api, name, email, hash); 44 | 45 | // generate node token for new User node 46 | const token = await graphcool.generateNodeToken(userId, 'User'); 47 | 48 | return { data: { id: userId, token } }; 49 | 50 | } catch (e) { 51 | console.log(e); 52 | return { error: 'An unexpected error occured during signup.' }; 53 | } 54 | }; 55 | 56 | async function getUser(api: GraphQLClient, email: string): Promise<{ User }> { 57 | const query = ` 58 | query getUser($email: String!) { 59 | User(email: $email) { 60 | id 61 | } 62 | } 63 | `; 64 | 65 | const variables = { 66 | email, 67 | }; 68 | 69 | return api.request<{ User }>(query, variables); 70 | } 71 | 72 | async function createGraphcoolUser(api: GraphQLClient, name: string, email: string, password: string): Promise { 73 | const mutation = ` 74 | mutation createGraphcoolUser($name: String!, $email: String!, $password: String!) { 75 | createUser( 76 | name: $name, 77 | email: $email, 78 | password: $password 79 | ) { 80 | id 81 | } 82 | } 83 | `; 84 | 85 | const variables = { 86 | name, 87 | email, 88 | password 89 | }; 90 | 91 | return api.request<{ createUser: User }>(mutation, variables) 92 | .then(r => r.createUser.id); 93 | } 94 | -------------------------------------------------------------------------------- /graphcool/src/permissions/User.graphql: -------------------------------------------------------------------------------- 1 | query permitUser($user_id: ID!, $node_id: ID!) { 2 | SomeUserExists(filter: { 3 | AND: [ 4 | { id: $user_id }, 5 | { id: $node_id } 6 | ] 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /graphcool/src/permissions/permitChangeMessage.graphql: -------------------------------------------------------------------------------- 1 | query permitChangeMessage($node_id: ID!, $user_id: ID!) { 2 | SomeMessageExists( 3 | filter: { 4 | id: $node_id, 5 | sender: { 6 | id: $user_id 7 | } 8 | } 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /graphcool/src/permissions/permitCreateMessage.graphql: -------------------------------------------------------------------------------- 1 | query permitCreateMessage($user_id: ID!, $input_senderId: ID!) { 2 | SomeMessageExists( 3 | filter: { 4 | sender: { 5 | AND: [ 6 | { 7 | id: $user_id 8 | }, 9 | { 10 | id: $input_senderId 11 | } 12 | ] 13 | } 14 | } 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /graphcool/src/permissions/permitReadAndChangeChat.graphql: -------------------------------------------------------------------------------- 1 | query permitReadAndChangeChat($node_id: ID!, $user_id: ID!) { 2 | SomeChatExists( 3 | filter: { 4 | id: $node_id, 5 | users_some: { 6 | id: $user_id 7 | } 8 | } 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /graphcool/src/permissions/permitReadMessage.graphql: -------------------------------------------------------------------------------- 1 | query permitReadMessage($node_id: ID!, $user_id: ID!) { 2 | SomeMessageExists( 3 | filter: { 4 | id: $node_id, 5 | chat: { 6 | users_some: { 7 | id: $user_id 8 | } 9 | } 10 | } 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /graphcool/src/permissions/permitSendMessageToChat.graphql: -------------------------------------------------------------------------------- 1 | query permitSendMessageToChat($user_id: ID!, $chatChat_id: ID!) { 2 | SomeChatExists( 3 | filter: { 4 | id: $chatChat_id, 5 | users_some: { 6 | id: $user_id 7 | } 8 | } 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /graphcool/types.graphql: -------------------------------------------------------------------------------- 1 | # The following types define the data model of the example service 2 | # based on which the GraphQL API is generated 3 | 4 | type User @model { 5 | id: ID! @isUnique 6 | name: String! 7 | email: String! @isUnique 8 | password: String! 9 | createdAt: DateTime! 10 | updatedAt: DateTime! 11 | chats: [Chat!]! @relation(name: "UsersOnChats") 12 | messages: [Message!]! @relation(name: "MessagesOnUser") 13 | photo: File @relation(name: "UserPhoto") 14 | dummy: String 15 | } 16 | 17 | type Chat @model { 18 | id: ID! @isUnique 19 | title: String 20 | isGroup: Boolean! @defaultValue(value: "false") 21 | createdAt: DateTime! 22 | updatedAt: DateTime! 23 | users: [User!]! @relation(name: "UsersOnChats") 24 | messages: [Message!]! @relation(name: "MessagesOnChat") 25 | photo: File @relation(name: "ChatPhoto") 26 | } 27 | 28 | type Message @model { 29 | id: ID! @isUnique 30 | text: String! 31 | createdAt: DateTime! 32 | sender: User! @relation(name: "MessagesOnUser") 33 | chat: Chat! @relation(name: "MessagesOnChat") 34 | } 35 | 36 | type File @model { 37 | id: ID! @isUnique 38 | secret: String 39 | name: String 40 | size: Int 41 | url: String 42 | contentType: String 43 | user: User @relation(name: "UserPhoto") 44 | chat: Chat @relation(name: "ChatPhoto") 45 | } 46 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | gulp.task('now:config', () => { 4 | return gulp 5 | .src(['now/package.json']) 6 | .pipe(gulp.dest('dist/angular-graphcool-chat')); 7 | }); 8 | 9 | gulp.task('default', gulp.series(['now:config'])); 10 | -------------------------------------------------------------------------------- /now/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-graphcool-chat", 3 | "version": "1.0.0", 4 | "description": "Angular Graphcool Chat", 5 | "author": "Plínio Naves ", 6 | "license": "ISC", 7 | "scripts": { 8 | "start": "serve --single" 9 | }, 10 | "dependencies": { 11 | "serve": "latest" 12 | }, 13 | "now": { 14 | "name": "angular-graphcool-chat", 15 | "alias": "angular-graphcool-chat", 16 | "static": { 17 | "rewrites": [ 18 | { 19 | "source": "*", 20 | "destination": "/index.html" 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-graphcool-chat", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "deploy": "ng build --prod && gulp && cd ./dist/angular-graphcool-chat && now --public && now alias" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^6.0.3", 16 | "@angular/cdk": "^6.1.0", 17 | "@angular/common": "^6.0.3", 18 | "@angular/compiler": "^6.0.3", 19 | "@angular/core": "^6.0.3", 20 | "@angular/forms": "^6.0.3", 21 | "@angular/http": "^6.0.3", 22 | "@angular/material": "^6.1.0", 23 | "@angular/platform-browser": "^6.0.3", 24 | "@angular/platform-browser-dynamic": "^6.0.3", 25 | "@angular/router": "^6.0.3", 26 | "apollo-angular": "1.1.0", 27 | "apollo-angular-link-http": "1.1.0", 28 | "apollo-cache-inmemory": "1.2.1", 29 | "apollo-cache-persist": "0.1.1", 30 | "apollo-client": "2.3.1", 31 | "apollo-link": "1.2.2", 32 | "apollo-link-error": "1.0.9", 33 | "apollo-link-ws": "1.0.8", 34 | "core-js": "^2.5.4", 35 | "graphql": "git://github.com/graphql/graphql-js.git#npm", 36 | "graphql-tag": "2.9.2", 37 | "hammerjs": "^2.0.8", 38 | "js-base64": "2.4.5", 39 | "rxjs": "^6.2.0", 40 | "smoothscroll-polyfill": "0.4.3", 41 | "subscriptions-transport-ws": "0.9.14", 42 | "zone.js": "^0.8.26" 43 | }, 44 | "devDependencies": { 45 | "@angular-devkit/build-angular": "0.6.3", 46 | "@angular/cli": "6.0.3", 47 | "@angular/compiler-cli": "^6.0.3", 48 | "@angular/language-service": "^6.0.3", 49 | "@types/jasmine": "~2.8.6", 50 | "@types/jasminewd2": "~2.0.3", 51 | "@types/js-base64": "2.3.1", 52 | "@types/node": "~8.9.4", 53 | "codelyzer": "~4.2.1", 54 | "gulp": "4.0.0", 55 | "jasmine-core": "~2.99.1", 56 | "jasmine-spec-reporter": "~4.2.1", 57 | "karma": "~1.7.1", 58 | "karma-chrome-launcher": "~2.2.0", 59 | "karma-coverage-istanbul-reporter": "~1.4.2", 60 | "karma-jasmine": "~1.1.1", 61 | "karma-jasmine-html-reporter": "^0.2.2", 62 | "now": "11.4.2", 63 | "protractor": "~5.3.0", 64 | "ts-node": "~5.0.1", 65 | "tslint": "~5.9.1", 66 | "typescript": "~2.7.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/apollo-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, NgModule } from '@angular/core'; 2 | import { HttpClientModule, HttpHeaders } from '@angular/common/http'; 3 | 4 | import { Apollo, ApolloModule } from 'apollo-angular'; 5 | import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http'; 6 | import { InMemoryCache } from 'apollo-cache-inmemory'; 7 | import { CachePersistor } from 'apollo-cache-persist'; 8 | import { ApolloLink, Operation } from 'apollo-link'; 9 | import { onError } from 'apollo-link-error'; 10 | import { WebSocketLink } from 'apollo-link-ws'; 11 | import { getOperationAST } from 'graphql'; 12 | import { SubscriptionClient } from 'subscriptions-transport-ws'; 13 | 14 | import { environment } from '../environments/environment'; 15 | import { GRAPHCOOL_CONFIG, GraphcoolConfig } from './core/providers/graphcool-config.provider'; 16 | import { StorageKeys } from './storage-keys'; 17 | 18 | @NgModule({ 19 | imports: [ 20 | HttpClientModule, 21 | ApolloModule, 22 | HttpLinkModule 23 | ] 24 | }) 25 | export class ApolloConfigModule { 26 | 27 | cachePersistor: CachePersistor; 28 | private subscriptionClient: SubscriptionClient; 29 | 30 | constructor( 31 | private apollo: Apollo, 32 | @Inject(GRAPHCOOL_CONFIG) private graphcoolConfig: GraphcoolConfig, 33 | private httpLink: HttpLink 34 | ) { 35 | 36 | const uri = this.graphcoolConfig.simpleAPI; 37 | const http = httpLink.create({ uri }); 38 | 39 | const authMiddleware: ApolloLink = new ApolloLink((operation, forward) => { 40 | operation.setContext({ 41 | headers: new HttpHeaders({ 42 | 'Authorization': `Bearer ${this.getAuthToken()}` 43 | }) 44 | }); 45 | return forward(operation); 46 | }); 47 | 48 | const linkError = onError(({ graphQLErrors, networkError }) => { 49 | if (graphQLErrors) { 50 | graphQLErrors.map(({ message, locations, path }) => 51 | console.log( 52 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, 53 | ), 54 | ); 55 | } 56 | 57 | if (networkError) { console.log(`[Network error]: ${networkError}`); } 58 | }); 59 | 60 | const ws = new WebSocketLink({ 61 | uri: this.graphcoolConfig.subscriptionsAPI, 62 | options: { 63 | reconnect: true, 64 | timeout: 30000, 65 | connectionParams: () => ({ 'Authorization': `Bearer ${this.getAuthToken()}` }) 66 | } 67 | }); 68 | 69 | this.subscriptionClient = (ws).subscriptionClient; 70 | 71 | const cache = new InMemoryCache(); 72 | 73 | this.cachePersistor = new CachePersistor({ 74 | cache, 75 | storage: window.localStorage 76 | }); 77 | 78 | apollo.create({ 79 | link: ApolloLink.from([ 80 | linkError, 81 | ApolloLink.split( 82 | (operation: Operation) => { 83 | const operationAST = getOperationAST(operation.query, operation.operationName); 84 | return !!operationAST && operationAST.operation === 'subscription'; 85 | }, 86 | ws, 87 | authMiddleware.concat(http) 88 | ) 89 | ]), 90 | cache, 91 | connectToDevTools: !environment.production 92 | }); 93 | 94 | } 95 | 96 | closeWebSocketConnection(): void { 97 | this.subscriptionClient.close(true, true); 98 | } 99 | 100 | private getAuthToken(): string { 101 | return window.localStorage.getItem(StorageKeys.AUTH_TOKEN); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { AuthenticatedPreloadingStrategy } from './core/strategy/authenticated-preloading.strategy'; 5 | import { AuthGuard } from './login/auth.guard'; 6 | import { NotFoundComponent } from './core/components/not-found/not-found.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'dashboard', 11 | loadChildren: './dashboard/dashboard.module#DashboardModule', 12 | canActivate: [ AuthGuard ] 13 | }, 14 | { path: '', redirectTo: 'login', pathMatch: 'full' }, 15 | { path: '**', component: NotFoundComponent } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [ 20 | RouterModule.forRoot(routes, { 21 | preloadingStrategy: AuthenticatedPreloadingStrategy 22 | }) 23 | ], 24 | exports: [ RouterModule ] 25 | }) 26 | export class AppRoutingModule {} 27 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material'; 3 | import { take } from 'rxjs/operators'; 4 | 5 | import { AppConfigService } from './core/services/app-config.service'; 6 | import { AuthService } from './core/services/auth.service'; 7 | import { ErrorService } from './core/services/error.service'; 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | template: ` 12 | 13 | ` 14 | }) 15 | export class AppComponent implements OnInit { 16 | 17 | constructor( 18 | private appConfig: AppConfigService, 19 | private authService: AuthService, 20 | private errorService: ErrorService, 21 | private snackBar: MatSnackBar 22 | ) { } 23 | 24 | ngOnInit(): void { 25 | this.authService.autoLogin() 26 | .pipe(take(1)) 27 | .subscribe( 28 | null, 29 | error => { 30 | const message = this.errorService.getErrorMessage(error); 31 | this.snackBar.open( 32 | `Error: ${message}`, 33 | 'Done', 34 | { duration: 5000, verticalPosition: 'top' } 35 | ); 36 | } 37 | ); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { CoreModule } from './core/core.module'; 6 | import { LoginModule } from './login/login.module'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | CoreModule, 14 | LoginModule, 15 | AppRoutingModule 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule { } 21 | -------------------------------------------------------------------------------- /src/app/chat/chat-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '../login/auth.guard'; 5 | import { ChatListComponent } from './components/chat-list/chat-list.component'; 6 | import { ChatTabComponent } from './components/chat-tab/chat-tab.component'; 7 | import { ChatUsersComponent } from './components/chat-users/chat-users.component'; 8 | import { ChatWindowComponent } from './components/chat-window/chat-window.component'; 9 | import { ChatWindowResolver } from './components/chat-window/chat-window.resolver'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: ChatTabComponent, 15 | canActivate: [ AuthGuard ], 16 | canActivateChild: [ AuthGuard ], 17 | children: [ 18 | { path: 'users', component: ChatUsersComponent }, 19 | { path: '', component: ChatListComponent } 20 | ] 21 | }, 22 | { 23 | path: ':id', 24 | component: ChatWindowComponent, 25 | canActivate: [ AuthGuard ], 26 | resolve: { chat: ChatWindowResolver } 27 | } 28 | ]; 29 | 30 | @NgModule({ 31 | imports: [RouterModule.forChild(routes)], 32 | exports: [RouterModule], 33 | providers: [ ChatWindowResolver ] 34 | }) 35 | export class ChatRoutingModule { } 36 | -------------------------------------------------------------------------------- /src/app/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { ChatAddGroupComponent } from './components/chat-add-group/chat-add-group.component'; 4 | import { ChatListComponent } from './components/chat-list/chat-list.component'; 5 | import { ChatMessageComponent } from './components/chat-message/chat-message.component'; 6 | import { ChatRoutingModule } from './chat-routing.module'; 7 | import { ChatTabComponent } from './components/chat-tab/chat-tab.component'; 8 | import { ChatUsersComponent } from './components/chat-users/chat-users.component'; 9 | import { ChatWindowComponent } from './components/chat-window/chat-window.component'; 10 | import { SharedModule } from '../shared/shared.module'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | SharedModule, 15 | ChatRoutingModule 16 | ], 17 | declarations: [ 18 | ChatAddGroupComponent, 19 | ChatListComponent, 20 | ChatMessageComponent, 21 | ChatTabComponent, 22 | ChatUsersComponent, 23 | ChatWindowComponent, 24 | ], 25 | entryComponents: [ 26 | ChatAddGroupComponent 27 | ] 28 | }) 29 | export class ChatModule { } 30 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-add-group/chat-add-group.component.html: -------------------------------------------------------------------------------- 1 |

New Group

2 | 3 | 4 | 5 |
6 | 9 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | This field is required 23 | 24 | 25 | Enter 3 at least characters 26 | 27 | 28 | 29 | 30 |

Selected users:

31 | 32 |

{{ member.value.name }}

33 | 36 |
37 | 38 |
39 | 40 | 41 | 42 |

Add users:

43 | 44 |

{{ user.name }}

45 | 48 |
49 |
50 | 51 |

No users found.

52 |
53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-add-group/chat-add-group.component.scss: -------------------------------------------------------------------------------- 1 | .group-cover { 2 | position: relative; 3 | .btn-choose-photo { 4 | position: absolute; 5 | right: 0; 6 | bottom: 0; 7 | background-color: white; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-add-group/chat-add-group.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatAddGroupComponent } from './chat-add-group.component'; 4 | 5 | describe('ChatAddGroupComponent', () => { 6 | let component: ChatAddGroupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatAddGroupComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatAddGroupComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-add-group/chat-add-group.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { MatDialogRef, MatSnackBar } from '@angular/material'; 4 | import { Observable, Subscription, of } from 'rxjs'; 5 | import { map, mergeMap, take } from 'rxjs/operators'; 6 | 7 | import { Chat } from '../../models/chat.model'; 8 | import { ChatService } from '../../services/chat.service'; 9 | import { ErrorService } from '../../../core/services/error.service'; 10 | import { FileModel } from '../../../core/models/file.model'; 11 | import { FileService } from '../../../core/services/file.service'; 12 | import { User } from '../../../core/models/user.model'; 13 | import { UserService } from '../../../core/services/user.service'; 14 | 15 | @Component({ 16 | selector: 'app-chat-add-group', 17 | templateUrl: './chat-add-group.component.html', 18 | styleUrls: ['./chat-add-group.component.scss'] 19 | }) 20 | export class ChatAddGroupComponent implements OnDestroy, OnInit { 21 | 22 | newGroupForm: FormGroup; 23 | selectedImage: File; 24 | users$: Observable; 25 | private subscriptions: Subscription[] = []; 26 | 27 | constructor( 28 | private chatService: ChatService, 29 | private dialogRef: MatDialogRef, 30 | private errorService: ErrorService, 31 | private fb: FormBuilder, 32 | private fileService: FileService, 33 | private snackBar: MatSnackBar, 34 | private userService: UserService 35 | ) { } 36 | 37 | ngOnInit(): void { 38 | this.users$ = this.userService.users$; 39 | this.createForm(); 40 | this.listenMembersList(); 41 | } 42 | 43 | private listenMembersList(): void { 44 | this.subscriptions.push( 45 | this.members.valueChanges 46 | .subscribe(() => { 47 | this.users$ = this.users$ 48 | .pipe( 49 | map(users => users.filter(user => this.members.controls.every(c => c.value.id !== user.id))) 50 | ); 51 | }) 52 | ); 53 | } 54 | 55 | private createForm(): void { 56 | this.newGroupForm = this.fb.group({ 57 | title: this.fb.control('', [Validators.required, Validators.minLength(3)]), 58 | members: this.fb.array([], Validators.required) 59 | }); 60 | } 61 | 62 | get title(): FormControl { return this.newGroupForm.get('title'); } 63 | get members(): FormArray { return this.newGroupForm.get('members'); } 64 | 65 | addMember(user: User): void { 66 | this.members.push(this.fb.group(user)); 67 | } 68 | 69 | removeMember(index: number): void { 70 | this.members.removeAt(index); 71 | } 72 | 73 | onSelectImage(event: Event): void { 74 | const file = (event.target).files[0]; 75 | this.selectedImage = file; 76 | } 77 | 78 | onSubmit(): void { 79 | 80 | let operation: Observable = of(null); 81 | 82 | if (this.selectedImage) { 83 | operation = this.fileService.upload(this.selectedImage); 84 | } 85 | 86 | let message: string; 87 | operation 88 | .pipe( 89 | mergeMap((uploadedImage: FileModel) => { 90 | const formValue = Object.assign({ 91 | title: this.title.value, 92 | usersIds: this.members.value.map(m => m.id), 93 | photoId: (uploadedImage) ? uploadedImage.id : null 94 | }); 95 | return this.chatService.createGroup(formValue); 96 | }), 97 | take(1) 98 | ).subscribe( 99 | (chat: Chat) => message = `'${chat.title}' created!`, 100 | (error) => message = this.errorService.getErrorMessage(error), 101 | () => { 102 | this.dialogRef.close(); 103 | this.snackBar.open(message, 'OK', { duration: 3000 }); 104 | } 105 | ); 106 | 107 | } 108 | 109 | ngOnDestroy(): void { 110 | this.subscriptions.forEach(s => s.unsubscribe()); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-list/chat-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 |

{{ getChatTitle(chat) }}

13 |

{{ getLastMessage(chat) }}

14 |
15 |
16 |
17 | 18 |
19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-list/chat-list.component.scss: -------------------------------------------------------------------------------- 1 | .fab-bottom-right { 2 | position: fixed; 3 | bottom: 15px; 4 | right: 15px; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-list/chat-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatListComponent } from './chat-list.component'; 4 | 5 | describe('ChatListComponent', () => { 6 | let component: ChatListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-list/chat-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialog } from '@angular/material'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { AuthService } from '../../../core/services/auth.service'; 6 | import { BaseComponent } from '../../../shared/components/base.component'; 7 | import { Chat } from '../../models/chat.model'; 8 | import { ChatAddGroupComponent } from '../chat-add-group/chat-add-group.component'; 9 | import { ChatService } from '../../services/chat.service'; 10 | 11 | @Component({ 12 | selector: 'app-chat-list', 13 | templateUrl: './chat-list.component.html', 14 | styleUrls: ['./chat-list.component.scss'] 15 | }) 16 | export class ChatListComponent extends BaseComponent implements OnInit { 17 | 18 | chats$: Observable; 19 | 20 | constructor( 21 | protected authService: AuthService, 22 | private chatService: ChatService, 23 | protected dialog: MatDialog 24 | ) { 25 | super(); 26 | } 27 | 28 | ngOnInit() { 29 | this.chats$ = this.chatService.chats$; 30 | } 31 | 32 | getChatTitle(chat: Chat): string { 33 | return chat.title || chat.users[0].name; 34 | } 35 | 36 | getLastMessage(chat: Chat): string { 37 | const message = chat.messages[0]; 38 | if (message) { 39 | const sender = 40 | (message.sender.id === this.authService.authUser.id) 41 | ? 'You' 42 | : message.sender.name; 43 | return `${sender}: ${message.text}`; 44 | } 45 | return 'No messages.'; 46 | } 47 | 48 | onAddGroup(): void { 49 | this.dialog.open(ChatAddGroupComponent, { width: '400px', maxHeight: '80vh' }); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-message/chat-message.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | 10 | {{ message.text }} 11 | {{ message.sender.name }}: {{ message.createdAt | fromNow }} 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-message/chat-message.component.scss: -------------------------------------------------------------------------------- 1 | @import "_variables.scss"; 2 | 3 | .message-container { 4 | margin: 20px 12px; 5 | display: flex; 6 | flex-flow: row wrap; 7 | } 8 | 9 | .text-container { 10 | flex: 1 0 80%; 11 | position: relative; 12 | margin-left: 20px; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: flex-start; 16 | .text { 17 | background-color: $gray; 18 | color: #555; 19 | box-shadow: 1px 1px 2px #ccc; 20 | padding: 3px 15px; 21 | } 22 | .time { 23 | color: #ccc; 24 | text-align: right; 25 | } 26 | } 27 | 28 | .arrow { 29 | width: 0; 30 | height: 0; 31 | border-style: solid; 32 | border-width: 0 10px 10px 0; 33 | display: inline-block; 34 | position: absolute; 35 | } 36 | 37 | .arrow-right { 38 | border-color: transparent $primary transparent transparent; 39 | transform: rotate(-90deg); 40 | right: -10px; 41 | } 42 | 43 | .arrow-left { 44 | border-color: transparent $gray transparent transparent; 45 | transform: rotate(0); 46 | left: -10px; 47 | } 48 | 49 | .sender-container { 50 | align-items: flex-end; 51 | margin-left: 0; 52 | margin-right: 20px; 53 | .text { 54 | background-color: $primary; 55 | box-shadow: -1px 1px 2px #ccc; 56 | color: #fff; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-message/chat-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatMessageComponent } from './chat-message.component'; 4 | 5 | describe('ChatMessageComponent', () => { 6 | let component: ChatMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatMessageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-message/chat-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | import { Message } from '../../models/message.model'; 4 | 5 | @Component({ 6 | selector: 'app-chat-message', 7 | templateUrl: './chat-message.component.html', 8 | styleUrls: ['./chat-message.component.scss'] 9 | }) 10 | export class ChatMessageComponent implements OnInit { 11 | 12 | @Input() message: Message; 13 | @Input() isFromSender: boolean; 14 | arrowClass = {}; 15 | 16 | ngOnInit(): void { 17 | this.arrowClass = { 18 | 'arrow-left': !this.isFromSender, 19 | 'arrow-right': this.isFromSender 20 | }; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-tab/chat-tab.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { AuthService } from '../../../core/services/auth.service'; 4 | import { ChatService } from '../../services/chat.service'; 5 | import { UserService } from '../../../core/services/user.service'; 6 | 7 | @Component({ 8 | selector: 'app-chat-tab', 9 | template: ` 10 | 30 | 31 | 32 | ` 33 | }) 34 | export class ChatTabComponent implements OnInit { 35 | 36 | constructor( 37 | private authService: AuthService, 38 | private chatService: ChatService, 39 | private userService: UserService 40 | ) {} 41 | 42 | ngOnInit(): void { 43 | this.chatService.startChatsMonitoring(); 44 | this.userService.startUsersMonitoring(this.authService.authUser.id); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-users/chat-users.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 |

{{ user.name }}

13 |

Member since {{ user.createdAt | date: 'dd/MM/yyyy' }}

14 |
15 |
16 |
17 | 18 |
19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-users/chat-users.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/app/chat/components/chat-users/chat-users.component.scss -------------------------------------------------------------------------------- /src/app/chat/components/chat-users/chat-users.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatUsersComponent } from './chat-users.component'; 4 | 5 | describe('ChatUsersComponent', () => { 6 | let component: ChatUsersComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatUsersComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatUsersComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-users/chat-users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | 4 | import { BaseComponent } from '../../../shared/components/base.component'; 5 | import { User } from '../../../core/models/user.model'; 6 | import { UserService } from '../../../core/services/user.service'; 7 | 8 | @Component({ 9 | selector: 'app-chat-users', 10 | templateUrl: './chat-users.component.html', 11 | styleUrls: ['./chat-users.component.scss'] 12 | }) 13 | export class ChatUsersComponent extends BaseComponent implements OnInit { 14 | 15 | users$: Observable; 16 | 17 | constructor( 18 | private userService: UserService 19 | ) { 20 | super(); 21 | } 22 | 23 | ngOnInit() { 24 | this.users$ = this.userService.users$; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-window/chat-window.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-window/chat-window.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | position: relative; 3 | padding-bottom: 50px; 4 | margin-bottom: -50px; 5 | } 6 | 7 | .input { 8 | flex: 1 1 90%; 9 | height: 21px; 10 | border: none; 11 | padding: 3px 10px; 12 | font-size: 16px; 13 | color: #555; 14 | background-color: #efefef; 15 | outline: none; 16 | resize: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-window/chat-window.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatWindowComponent } from './chat-window.component'; 4 | 5 | describe('ChatWindowComponent', () => { 6 | let component: ChatWindowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatWindowComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatWindowComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-window/chat-window.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; 2 | import { ActivatedRoute, ParamMap } from '@angular/router'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { Observable, Subscription, of } from 'rxjs'; 5 | import { map, mergeMap, tap, take } from 'rxjs/operators'; 6 | 7 | import { AuthService } from '../../../core/services/auth.service'; 8 | import { BaseComponent } from '../../../shared/components/base.component'; 9 | import { Chat } from '../../models/chat.model'; 10 | import { ChatMessageComponent } from '../chat-message/chat-message.component'; 11 | import { ChatService } from '../../services/chat.service'; 12 | import { Message } from '../../models/message.model'; 13 | import { MessageService } from '../../services/message.service'; 14 | import { User } from '../../../core/models/user.model'; 15 | import { UserService } from '../../../core/services/user.service'; 16 | 17 | @Component({ 18 | selector: 'app-chat-window', 19 | templateUrl: './chat-window.component.html', 20 | styleUrls: ['./chat-window.component.scss'] 21 | }) 22 | export class ChatWindowComponent extends BaseComponent implements AfterViewInit, OnDestroy, OnInit { 23 | 24 | chat: Chat; 25 | messages$: Observable; 26 | newMessage = ''; 27 | recipientId: string = null; 28 | alreadyLoadedMessages = false; 29 | @ViewChild('content') private content: ElementRef; 30 | @ViewChildren(ChatMessageComponent) private messagesQueryList: QueryList; 31 | private subscriptions: Subscription[] = []; 32 | 33 | constructor( 34 | public authService: AuthService, 35 | private chatService: ChatService, 36 | private messageService: MessageService, 37 | private route: ActivatedRoute, 38 | private title: Title, 39 | private userService: UserService 40 | ) { 41 | super(); 42 | } 43 | 44 | ngOnInit(): void { 45 | this.chatService.startChatsMonitoring(); 46 | this.userService.startUsersMonitoring(this.authService.authUser.id); 47 | this.title.setTitle('Loading...'); 48 | this.subscriptions.push( 49 | this.route.data 50 | .pipe( 51 | map(routeData => this.chat = routeData.chat), 52 | mergeMap(() => this.route.paramMap), 53 | tap((params: ParamMap) => { 54 | if (!this.chat) { 55 | this.recipientId = params.get('id'); 56 | 57 | this.userService.getUserById(this.recipientId) 58 | .pipe(take(1)) 59 | .subscribe((user: User) => this.title.setTitle(user.name)); 60 | 61 | this.messages$ = of([]); 62 | 63 | } else { 64 | this.title.setTitle(this.chat.title || this.chat.users[0].name); 65 | this.messages$ = this.messageService.getChatMessages(this.chat.id); 66 | this.alreadyLoadedMessages = true; 67 | } 68 | }) 69 | ) 70 | .subscribe() 71 | ); 72 | } 73 | 74 | ngAfterViewInit(): void { 75 | this.subscriptions.push( 76 | this.messagesQueryList.changes.subscribe(() => { 77 | this.scrollToBottom('smooth'); 78 | }) 79 | ); 80 | } 81 | 82 | sendMessage(): void { 83 | this.newMessage = this.newMessage.trim(); 84 | if (this.newMessage) { 85 | 86 | if (this.chat) { 87 | 88 | this.createMessage() 89 | .pipe(take(1)).subscribe(); 90 | 91 | this.newMessage = ''; 92 | 93 | } else { 94 | this.createPrivateChat(); 95 | } 96 | 97 | } 98 | } 99 | 100 | private createMessage(): Observable { 101 | return this.messageService.createMessage({ 102 | text: this.newMessage, 103 | chatId: this.chat.id, 104 | senderId: this.authService.authUser.id 105 | }).pipe( 106 | tap(message => { 107 | if (!this.alreadyLoadedMessages) { 108 | this.messages$ = this.messageService.getChatMessages(this.chat.id); 109 | this.alreadyLoadedMessages = true; 110 | } 111 | }) 112 | ); 113 | } 114 | 115 | private createPrivateChat(): void { 116 | this.chatService.createPrivateChat(this.recipientId) 117 | .pipe( 118 | take(1), 119 | tap((chat: Chat) => { 120 | this.chat = chat; 121 | this.sendMessage(); 122 | }) 123 | ).subscribe(); 124 | } 125 | 126 | private scrollToBottom(behavior: string = 'auto', block: string = 'end'): void { 127 | setTimeout(() => { 128 | this.content.nativeElement.scrollIntoView({ behavior, block }); 129 | }, 0); 130 | } 131 | 132 | ngOnDestroy(): void { 133 | this.subscriptions.forEach(s => s.unsubscribe()); 134 | this.title.setTitle('Angular Graphcool Chat'); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/app/chat/components/chat-window/chat-window.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { Chat } from '../../models/chat.model'; 7 | import { ChatService } from '../../services/chat.service'; 8 | import { ErrorService } from '../../../core/services/error.service'; 9 | 10 | @Injectable() 11 | export class ChatWindowResolver implements Resolve { 12 | 13 | constructor( 14 | private chatService: ChatService, 15 | private errorService: ErrorService, 16 | private router: Router 17 | ) {} 18 | 19 | resolve( 20 | route: ActivatedRouteSnapshot, 21 | state: RouterStateSnapshot 22 | ): Observable { 23 | const chatOrUserId: string = route.paramMap.get('id'); 24 | return this.chatService.getChatByIdOrByUsers(chatOrUserId) 25 | .pipe( 26 | catchError((error: Error) => { 27 | const errorMessage: string = this.errorService.getErrorMessage(error); 28 | let redirect = '/dashboard'; 29 | if (errorMessage.includes('Insufficient Permissions')) { 30 | redirect = '/dashboard/permission-denied'; 31 | } 32 | this.router.navigate([redirect], { queryParams: { previous: state.url } }); 33 | return of(null); 34 | }) 35 | ); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/chat/models/chat.model.ts: -------------------------------------------------------------------------------- 1 | import { FileModel } from '../../core/models/file.model'; 2 | import { Message } from './message.model'; 3 | import { User } from '../../core/models/user.model'; 4 | 5 | import { graphcoolConfig } from './../../core/providers/graphcool-config.provider'; 6 | 7 | export class Chat { 8 | 9 | id: string; 10 | createdAt?: string; 11 | isGroup?: boolean; 12 | title?: string; 13 | users?: User[]; 14 | messages?: Message[]; 15 | photo?: FileModel; 16 | 17 | constructor(chat: Chat) { 18 | Object.keys(chat) 19 | .forEach(key => this[key] = chat[key]); 20 | } 21 | 22 | getPhotoURL?(): string { 23 | if (this.photo && this.photo.secret) { 24 | return `${graphcoolConfig.fileDownloadURL}/${this.photo.secret}`; 25 | } 26 | if (this.isGroup) { 27 | return 'assets/images/group-no-photo.png'; 28 | } 29 | return this.users[0].getPhotoURL() || 'assets/images/user-no-photo.png'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/chat/models/message.model.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from './chat.model'; 2 | import { User } from '../../core/models/user.model'; 3 | 4 | export interface Message { 5 | id: string; 6 | text?: string; 7 | createdAt?: string; 8 | sender?: User; 9 | chat?: Chat; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/chat/services/chat.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { Chat } from '../models/chat.model'; 4 | import { FileFragment } from '../../core/services/file.graphql'; 5 | 6 | export interface AllChatsQuery { 7 | allChats: Chat[]; 8 | } 9 | 10 | export interface ChatQuery { 11 | Chat: Chat; 12 | } 13 | 14 | const ChatFragment = gql` 15 | fragment ChatFragment on Chat { 16 | id 17 | title 18 | createdAt 19 | isGroup 20 | photo { 21 | ...FileFragment 22 | } 23 | users( 24 | first: 1, 25 | filter: { 26 | id_not: $loggedUserId 27 | } 28 | ) { 29 | id 30 | name 31 | email 32 | createdAt 33 | photo { 34 | ...FileFragment 35 | } 36 | } 37 | } 38 | ${FileFragment} 39 | `; 40 | 41 | const ChatMessagesFragment = gql` 42 | fragment ChatMessagesFragment on Chat { 43 | messages( 44 | last: 1 45 | ) { 46 | id 47 | createdAt 48 | text 49 | sender { 50 | id 51 | name 52 | } 53 | } 54 | } 55 | `; 56 | 57 | export const USER_CHATS_QUERY = gql` 58 | query UserChatsQuery($loggedUserId: ID!) { 59 | allChats( 60 | filter: { 61 | users_some: { 62 | id: $loggedUserId 63 | } 64 | } 65 | ) { 66 | ...ChatFragment 67 | ...ChatMessagesFragment 68 | } 69 | } 70 | ${ChatFragment} 71 | ${ChatMessagesFragment} 72 | `; 73 | 74 | export const CHAT_BY_ID_OR_BY_USERS_QUERY = gql` 75 | query ChatByIdOrByUsersQuery($chatId: ID!, $loggedUserId: ID!, $targetUserId: ID!) { 76 | 77 | Chat( 78 | id: $chatId 79 | ) { 80 | ...ChatFragment 81 | } 82 | 83 | allChats( 84 | filter: { 85 | AND: [ 86 | { users_some: { id: $loggedUserId } }, 87 | { users_some: { id: $targetUserId } } 88 | ], 89 | isGroup: false 90 | } 91 | ) { 92 | ...ChatFragment 93 | } 94 | 95 | } 96 | ${ChatFragment} 97 | `; 98 | 99 | export const CREATE_PRIVATE_CHAT_MUTATION = gql` 100 | mutation CreatePrivateChatMutation($loggedUserId: ID!, $targetUserId: ID!) { 101 | createChat( 102 | usersIds: [ 103 | $loggedUserId, 104 | $targetUserId 105 | ] 106 | ) { 107 | ...ChatFragment 108 | ...ChatMessagesFragment 109 | } 110 | } 111 | ${ChatFragment} 112 | ${ChatMessagesFragment} 113 | `; 114 | 115 | export const CREATE_GROUP_MUTATION = gql` 116 | mutation CreateGroupMutation($title: String!, $usersIds: [ID!]!, $loggedUserId: ID!, $photoId: ID) { 117 | createChat( 118 | title: $title, 119 | usersIds: $usersIds 120 | isGroup: true, 121 | photoId: $photoId 122 | ) { 123 | ...ChatFragment 124 | ...ChatMessagesFragment 125 | } 126 | } 127 | ${ChatFragment} 128 | ${ChatMessagesFragment} 129 | `; 130 | 131 | export const USER_CHATS_SUBSCRIPTION = gql` 132 | subscription UserChatsSubscription($loggedUserId: ID!) { 133 | Chat( 134 | filter: { 135 | mutation_in: [ CREATED, UPDATED ], 136 | node: { 137 | users_some: { 138 | id: $loggedUserId 139 | } 140 | } 141 | } 142 | ) { 143 | mutation 144 | node { 145 | ...ChatFragment 146 | ...ChatMessagesFragment 147 | } 148 | } 149 | } 150 | ${ChatFragment} 151 | ${ChatMessagesFragment} 152 | `; 153 | -------------------------------------------------------------------------------- /src/app/chat/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NavigationEnd, Router, RouterEvent } from '@angular/router'; 3 | import { Apollo, QueryRef } from 'apollo-angular'; 4 | import { DataProxy } from 'apollo-cache'; 5 | import { Observable, Subscription } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | import { AuthService } from '../../core/services/auth.service'; 9 | import { BaseService } from '../../core/services/base.service'; 10 | import { 11 | CHAT_BY_ID_OR_BY_USERS_QUERY, 12 | CREATE_GROUP_MUTATION, 13 | CREATE_PRIVATE_CHAT_MUTATION, 14 | USER_CHATS_QUERY, 15 | USER_CHATS_SUBSCRIPTION, 16 | AllChatsQuery, 17 | ChatQuery 18 | } from './chat.graphql'; 19 | import { GET_CHAT_MESSAGES_QUERY, USER_MESSAGES_SUBSCRIPTION, AllMessagesQuery } from './message.graphql'; 20 | import { Chat } from '../models/chat.model'; 21 | import { Message } from '../models/message.model'; 22 | import { User } from '../../core/models/user.model'; 23 | import { UserService } from '../../core/services/user.service'; 24 | 25 | @Injectable({ 26 | providedIn: 'root' 27 | }) 28 | export class ChatService extends BaseService { 29 | 30 | chats$: Observable; 31 | private queryRef: QueryRef; 32 | private subscriptions: Subscription[] = []; 33 | 34 | constructor( 35 | private apollo: Apollo, 36 | private authService: AuthService, 37 | private router: Router, 38 | private userService: UserService 39 | ) { 40 | super(); 41 | } 42 | 43 | startChatsMonitoring(): void { 44 | if (!this.chats$) { 45 | this.chats$ = this.getUserChats(); 46 | this.subscriptions.push(this.chats$.subscribe()); 47 | this.subscriptions.push( 48 | this.router.events.subscribe((event: RouterEvent) => { 49 | if (event instanceof NavigationEnd && !this.router.url.includes('chat')) { 50 | this.stopChatsMonitoring(); 51 | this.userService.stopUsersMonitoring(); 52 | } 53 | }) 54 | ); 55 | } 56 | } 57 | 58 | private stopChatsMonitoring(): void { 59 | this.subscriptions.forEach(s => s.unsubscribe()); 60 | this.subscriptions = []; 61 | this.chats$ = null; 62 | } 63 | 64 | getUserChats(): Observable { 65 | this.queryRef = this.apollo.watchQuery({ 66 | query: USER_CHATS_QUERY, 67 | variables: { 68 | loggedUserId: this.authService.authUser.id 69 | }, 70 | fetchPolicy: 'network-only' 71 | }); 72 | 73 | this.queryRef.subscribeToMore({ 74 | document: USER_CHATS_SUBSCRIPTION, 75 | variables: { loggedUserId: this.authService.authUser.id }, 76 | updateQuery: (previous: AllChatsQuery, { subscriptionData }): AllChatsQuery => { 77 | 78 | const newChat: Chat = subscriptionData.data.Chat.node; 79 | 80 | if (previous.allChats.every(chat => chat.id !== newChat.id)) { 81 | return { 82 | ...previous, 83 | allChats: [newChat, ...previous.allChats] 84 | }; 85 | } 86 | 87 | return previous; 88 | } 89 | }); 90 | 91 | this.queryRef.subscribeToMore({ 92 | document: USER_MESSAGES_SUBSCRIPTION, 93 | variables: { loggedUserId: this.authService.authUser.id }, 94 | updateQuery: (previous: AllChatsQuery, { subscriptionData }): AllChatsQuery => { 95 | 96 | const newMessage: Message = subscriptionData.data.Message.node; 97 | 98 | try { 99 | 100 | if (newMessage.sender.id !== this.authService.authUser.id) { 101 | const apolloClient = this.apollo.getClient(); 102 | 103 | const chatMessagesVariables = { chatId: newMessage.chat.id }; 104 | 105 | const chatMessagesData = apolloClient.readQuery({ 106 | query: GET_CHAT_MESSAGES_QUERY, 107 | variables: chatMessagesVariables 108 | }); 109 | 110 | chatMessagesData.allMessages = [...chatMessagesData.allMessages, newMessage]; 111 | 112 | apolloClient.writeQuery({ 113 | query: GET_CHAT_MESSAGES_QUERY, 114 | variables: chatMessagesVariables, 115 | data: chatMessagesData 116 | }); 117 | } 118 | 119 | } catch (e) { 120 | console.log('allMessagesQuery not found!'); 121 | } 122 | 123 | 124 | const chatToUpdateIndex: number = 125 | (previous.allChats) 126 | ? previous.allChats.findIndex(chat => chat.id === newMessage.chat.id) 127 | : -1; 128 | 129 | if (chatToUpdateIndex > -1) { 130 | const newAllChats = [...previous.allChats]; 131 | const chatToUpdate: Chat = Object.assign({}, newAllChats[chatToUpdateIndex]); 132 | chatToUpdate.messages = [newMessage]; 133 | newAllChats[chatToUpdateIndex] = chatToUpdate; 134 | return { 135 | ...previous, 136 | allChats: newAllChats 137 | }; 138 | } 139 | 140 | return previous; 141 | } 142 | }); 143 | 144 | return this.queryRef.valueChanges 145 | .pipe( 146 | map(res => res.data.allChats), 147 | map((chats: Chat[]) => { 148 | const chatsToSort = chats.slice(); 149 | return chatsToSort.sort((a, b) => { 150 | const valueA = (a.messages.length > 0) ? new Date(a.messages[0].createdAt).getTime() : new Date(a.createdAt).getTime(); 151 | const valueB = (b.messages.length > 0) ? new Date(b.messages[0].createdAt).getTime() : new Date(b.createdAt).getTime(); 152 | return valueB - valueA; 153 | }); 154 | }), 155 | map(chats => chats.map(c => { 156 | const chat = new Chat(c); 157 | chat.users = chat.users.map(u => new User(u)); 158 | return chat; 159 | })) 160 | ); 161 | } 162 | 163 | getChatByIdOrByUsers(chatOrUserId: string): Observable { 164 | return this.apollo.query({ 165 | query: CHAT_BY_ID_OR_BY_USERS_QUERY, 166 | variables: { 167 | chatId: chatOrUserId, 168 | loggedUserId: this.authService.authUser.id, 169 | targetUserId: chatOrUserId 170 | } 171 | }).pipe( 172 | map(res => (res.data['Chat']) ? res.data['Chat'] : res.data['allChats'][0]) 173 | ); 174 | } 175 | 176 | createPrivateChat(targetUserId: string): Observable { 177 | return this.apollo.mutate({ 178 | mutation: CREATE_PRIVATE_CHAT_MUTATION, 179 | variables: { 180 | loggedUserId: this.authService.authUser.id, 181 | targetUserId 182 | }, 183 | update: (store: DataProxy, { data: { createChat } }) => { 184 | 185 | this.readAndWriteQuery({ 186 | store, 187 | newRecord: createChat, 188 | query: USER_CHATS_QUERY, 189 | queryName: 'allChats', 190 | arrayOperation: 'unshift', 191 | variables: { loggedUserId: this.authService.authUser.id } 192 | }); 193 | 194 | this.readAndWriteQuery({ 195 | store, 196 | newRecord: createChat, 197 | query: CHAT_BY_ID_OR_BY_USERS_QUERY, 198 | queryName: 'allChats', 199 | arrayOperation: 'singleRecord', 200 | variables: { 201 | chatId: targetUserId, 202 | loggedUserId: this.authService.authUser.id, 203 | targetUserId 204 | } 205 | }); 206 | 207 | } 208 | }).pipe( 209 | map(res => res.data.createChat) 210 | ); 211 | } 212 | 213 | createGroup(variables: {title: string, usersIds: string[], photoId: string}): Observable { 214 | 215 | variables.usersIds.push(this.authService.authUser.id); 216 | 217 | return this.apollo.mutate({ 218 | mutation: CREATE_GROUP_MUTATION, 219 | variables: { 220 | ...variables, 221 | loggedUserId: this.authService.authUser.id 222 | }, 223 | optimisticResponse: { 224 | __typename: 'Mutation', 225 | createChat: { 226 | __typename: 'Chat', 227 | id: '', 228 | title: variables.title, 229 | createdAt: new Date().toISOString(), 230 | isGroup: true, 231 | photo: { 232 | __typename: 'File', 233 | id: '', 234 | secret: '' 235 | }, 236 | users: [ 237 | { 238 | __typename: 'User', 239 | id: '', 240 | name: '', 241 | email: '', 242 | createdAt: new Date().toISOString(), 243 | photo: { 244 | __typename: 'File', 245 | id: '', 246 | secret: '' 247 | } 248 | } 249 | ], 250 | messages: [] 251 | } 252 | }, 253 | update: (store: DataProxy, { data: { createChat } }) => { 254 | 255 | this.readAndWriteQuery({ 256 | store, 257 | newRecord: createChat, 258 | query: USER_CHATS_QUERY, 259 | queryName: 'allChats', 260 | arrayOperation: 'unshift', 261 | variables: { loggedUserId: this.authService.authUser.id } 262 | }); 263 | 264 | } 265 | }).pipe( 266 | map(res => res.data.createChat) 267 | ); 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /src/app/chat/services/message.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | import { FileFragment } from '../../core/services/file.graphql'; 4 | import { Message } from '../models/message.model'; 5 | 6 | export interface AllMessagesQuery { 7 | allMessages: Message[]; 8 | } 9 | 10 | const MessageFragment = gql` 11 | fragment MessageFragment on Message { 12 | id 13 | text 14 | createdAt 15 | sender { 16 | id 17 | name 18 | email 19 | createdAt 20 | photo { 21 | ...FileFragment 22 | } 23 | } 24 | chat { 25 | id 26 | } 27 | } 28 | ${FileFragment} 29 | `; 30 | 31 | export const GET_CHAT_MESSAGES_QUERY = gql` 32 | query GetChatMessagesQuery($chatId: ID!) { 33 | allMessages( 34 | filter: { 35 | chat: { 36 | id: $chatId 37 | } 38 | }, 39 | orderBy: createdAt_ASC 40 | ) { 41 | ...MessageFragment 42 | } 43 | } 44 | ${MessageFragment} 45 | `; 46 | 47 | export const CREATE_MESSAGE_MUTATION = gql` 48 | mutation CreateMessageMutation($text: String!, $chatId: ID!, $senderId: ID!) { 49 | createMessage( 50 | text: $text, 51 | chatId: $chatId, 52 | senderId: $senderId 53 | ) { 54 | ...MessageFragment 55 | } 56 | } 57 | ${MessageFragment} 58 | `; 59 | 60 | export const USER_MESSAGES_SUBSCRIPTION = gql` 61 | subscription UserMessagesSubscription($loggedUserId: ID!) { 62 | Message( 63 | filter: { 64 | mutation_in: [ CREATED ], 65 | node: { 66 | chat: { 67 | users_some: { 68 | id: $loggedUserId 69 | } 70 | } 71 | } 72 | } 73 | ) { 74 | mutation 75 | node { 76 | ...MessageFragment 77 | } 78 | } 79 | } 80 | ${MessageFragment} 81 | `; 82 | -------------------------------------------------------------------------------- /src/app/chat/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Apollo } from 'apollo-angular'; 3 | import { DataProxy } from 'apollo-cache'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { AllChatsQuery, USER_CHATS_QUERY } from './chat.graphql'; 8 | import { AuthService } from '../../core/services/auth.service'; 9 | import { BaseService } from '../../core/services/base.service'; 10 | import { 11 | AllMessagesQuery, 12 | CREATE_MESSAGE_MUTATION, 13 | GET_CHAT_MESSAGES_QUERY 14 | } from './message.graphql'; 15 | import { Message } from '../models/message.model'; 16 | import { User } from '../../core/models/user.model'; 17 | 18 | @Injectable({ 19 | providedIn: 'root' 20 | }) 21 | export class MessageService extends BaseService { 22 | 23 | constructor( 24 | private apollo: Apollo, 25 | private authService: AuthService 26 | ) { 27 | super(); 28 | } 29 | 30 | getChatMessages(chatId: string): Observable { 31 | return this.apollo.watchQuery({ 32 | query: GET_CHAT_MESSAGES_QUERY, 33 | variables: { chatId }, 34 | fetchPolicy: 'network-only' 35 | }).valueChanges 36 | .pipe( 37 | map(res => res.data.allMessages), 38 | map(messages => messages.map(m => { 39 | const message = Object.assign({}, m); 40 | message.sender = new User(message.sender); 41 | return message; 42 | })) 43 | ); 44 | } 45 | 46 | createMessage(message: {text: string, chatId: string, senderId: string}): Observable { 47 | return this.apollo.mutate({ 48 | mutation: CREATE_MESSAGE_MUTATION, 49 | variables: message, 50 | optimisticResponse: { 51 | __typename: 'Mutation', 52 | createMessage: { 53 | __typename: 'Message', 54 | id: '', 55 | text: message.text, 56 | createdAt: new Date().toISOString(), 57 | sender: { 58 | __typename: 'User', 59 | id: message.senderId, 60 | name: this.authService.authUser.name, 61 | email: '', 62 | createdAt: '', 63 | photo: { 64 | __typename: 'File', 65 | id: '', 66 | secret: this.authService.authUser.photo && this.authService.authUser.photo.secret || '' 67 | } 68 | }, 69 | chat: { 70 | __typename: 'Chat', 71 | id: message.chatId 72 | } 73 | } 74 | }, 75 | update: (store: DataProxy, {data: { createMessage }}) => { 76 | 77 | this.readAndWriteQuery({ 78 | store, 79 | newRecord: createMessage, 80 | query: GET_CHAT_MESSAGES_QUERY, 81 | queryName: 'allMessages', 82 | arrayOperation: 'push', 83 | variables: { chatId: message.chatId } 84 | }); 85 | 86 | 87 | try { 88 | 89 | const userChatsVariables = { loggedUserId: this.authService.authUser.id }; 90 | 91 | const userChatsData = store.readQuery({ 92 | query: USER_CHATS_QUERY, 93 | variables: userChatsVariables 94 | }); 95 | 96 | const newUserChatsList = [...userChatsData.allChats]; 97 | 98 | newUserChatsList.map(c => { 99 | if (c.id === createMessage.chat.id) { 100 | c.messages = [createMessage]; 101 | } 102 | return c; 103 | }); 104 | 105 | userChatsData.allChats = newUserChatsList; 106 | 107 | store.writeQuery({ 108 | query: USER_CHATS_QUERY, 109 | variables: userChatsVariables, 110 | data: userChatsData 111 | }); 112 | 113 | } catch (e) { 114 | console.log(`Query allChats not found in cache!`); 115 | } 116 | 117 | } 118 | }).pipe( 119 | map(res => res.data.createMessage) 120 | ); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/app/core/components/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotFoundComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/components/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | 3 | import { AuthService } from '../../services/auth.service'; 4 | 5 | @Component({ 6 | selector: 'app-not-found', 7 | template: ` 8 | Angular Graphcool Chat 9 | 10 |

OOPS!

11 |

Error 404, page not found!

12 | Back to home 13 |
14 | `, 15 | styles: [] 16 | }) 17 | export class NotFoundComponent implements OnDestroy { 18 | 19 | constructor( 20 | private authService: AuthService 21 | ) { } 22 | 23 | ngOnDestroy(): void { 24 | this.authService.redirectUrl = null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/core.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from './core.module'; 2 | 3 | describe('CoreModule', () => { 4 | let coreModule: CoreModule; 5 | 6 | beforeEach(() => { 7 | coreModule = new CoreModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(coreModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { RouterModule } from '@angular/router'; 4 | import { Title } from '@angular/platform-browser'; 5 | 6 | import { ApolloConfigModule } from './../apollo-config.module'; 7 | import { NotFoundComponent } from './components/not-found/not-found.component'; 8 | import { SharedModule } from '../shared/shared.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | SharedModule, 13 | RouterModule 14 | ], 15 | exports: [ 16 | BrowserAnimationsModule, 17 | ApolloConfigModule 18 | ], 19 | providers: [ 20 | Title 21 | ], 22 | declarations: [ 23 | NotFoundComponent 24 | ] 25 | }) 26 | export class CoreModule { 27 | 28 | constructor( 29 | @Optional() @SkipSelf() parentModule: CoreModule 30 | ) { 31 | if (parentModule) { 32 | throw new Error('CoreModule is already loaded. Import it in the AppModule only.'); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/core/models/file.model.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from '../../chat/models/chat.model'; 2 | import { User } from './user.model'; 3 | 4 | export interface FileModel { 5 | id: string; 6 | secret?: string; 7 | name?: string; 8 | size?: number; 9 | url?: string; 10 | contentType?: string; 11 | user?: User; 12 | chat?: Chat; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/core/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { FileModel } from './file.model'; 2 | import { graphcoolConfig } from './../providers/graphcool-config.provider'; 3 | 4 | export class User { 5 | 6 | id: string; 7 | name?: string; 8 | email?: string; 9 | createdAt?: string; 10 | photo?: FileModel; 11 | 12 | constructor(user: User) { 13 | Object.keys(user) 14 | .forEach(key => this[key] = user[key]); 15 | } 16 | 17 | getPhotoURL?(): string { 18 | return (this.photo && this.photo.secret) 19 | ? `${graphcoolConfig.fileDownloadURL}/${this.photo.secret}` 20 | : 'assets/images/user-no-photo.png'; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/core/providers/graphcool-config.provider.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | import { graphcoolProjectInfo } from './../../../../graphcool/project-info/graphcool-project-info'; 4 | 5 | const graphcoolId = graphcoolProjectInfo.id; 6 | 7 | export interface GraphcoolConfig { 8 | simpleAPI: string; 9 | subscriptionsAPI: string; 10 | fileAPI: string; 11 | fileDownloadURL: string; 12 | } 13 | 14 | export const graphcoolConfig: GraphcoolConfig = { 15 | simpleAPI: `https://api.graph.cool/simple/v1/${graphcoolId}`, 16 | subscriptionsAPI: `wss://subscriptions.graph.cool/v1/${graphcoolId}`, 17 | fileAPI: `https://api.graph.cool/file/v1/${graphcoolId}`, 18 | fileDownloadURL: `https://files.graph.cool/${graphcoolId}` 19 | }; 20 | 21 | export const GRAPHCOOL_CONFIG = new InjectionToken( 22 | 'graphcool.config', 23 | { 24 | providedIn: 'root', 25 | factory: () => { 26 | return graphcoolConfig; 27 | } 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /src/app/core/services/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { take } from 'rxjs/operators'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AppConfigService { 9 | 10 | timeDifference: number; 11 | 12 | constructor( 13 | private http: HttpClient 14 | ) { 15 | this.getDifference(); 16 | } 17 | 18 | getDifference(): void { 19 | if (!this.timeDifference) { 20 | this.http.get('https://time-api.now.sh/current-time') 21 | .pipe(take(1)) 22 | .subscribe(res => { 23 | this.timeDifference = new Date(res['ISO']).getTime() - Date.now(); 24 | }); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/core/services/auth.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export interface LoggedInUserQuery { 4 | loggedInUser: { id: string }; 5 | } 6 | 7 | export const LOGGED_IN_USER_QUERY = gql` 8 | query LoggedInUserQuery { 9 | loggedInUser { 10 | id 11 | } 12 | } 13 | `; 14 | 15 | export const AUTHENTICATE_USER_MUTATION = gql` 16 | mutation AuthenticateUserMutation($email: String!, $password: String!) { 17 | authenticateUser(email: $email, password: $password) { 18 | id 19 | token 20 | } 21 | } 22 | `; 23 | 24 | export const SIGNUP_USER_MUTATION = gql` 25 | mutation SignupUserMutation($name: String!, $email: String!, $password: String!) { 26 | signupUser(name: $name, email: $email, password: $password) { 27 | id 28 | token 29 | } 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/app/core/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable, ReplaySubject, of, throwError } from 'rxjs'; 4 | import { catchError, map, mergeMap, take, tap } from 'rxjs/operators'; 5 | import { Apollo } from 'apollo-angular'; 6 | import { Base64 } from 'js-base64'; 7 | 8 | import { ApolloConfigModule } from '../../apollo-config.module'; 9 | import { AUTHENTICATE_USER_MUTATION, SIGNUP_USER_MUTATION, LoggedInUserQuery, LOGGED_IN_USER_QUERY } from './auth.graphql'; 10 | import { StorageKeys } from '../../storage-keys'; 11 | import { User } from '../models/user.model'; 12 | import { UserService } from './user.service'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class AuthService { 18 | 19 | authUser: User; 20 | redirectUrl: string; 21 | keepSigned: boolean; 22 | rememberMe: boolean; 23 | private _isAuthenticated = new ReplaySubject(1); 24 | 25 | constructor( 26 | private apollo: Apollo, 27 | private apolloConfigModule: ApolloConfigModule, 28 | private router: Router, 29 | private userService: UserService 30 | ) { 31 | this.init(); 32 | } 33 | 34 | init(): void { 35 | this.keepSigned = JSON.parse(window.localStorage.getItem(StorageKeys.KEEP_SIGNED)); 36 | this.rememberMe = JSON.parse(window.localStorage.getItem(StorageKeys.REMEMBER_ME)); 37 | } 38 | 39 | get isAuthenticated(): Observable { 40 | return this._isAuthenticated.asObservable(); 41 | } 42 | 43 | signinUser(variables: {email: string, password: string}): Observable<{id: string, token: string}> { 44 | return this.apollo.mutate({ 45 | mutation: AUTHENTICATE_USER_MUTATION, 46 | variables 47 | }).pipe( 48 | map(res => res.data.authenticateUser), 49 | tap(res => this.setAuthState({id: res && res.id, token: res && res.token, isAuthenticated: res !== null})), 50 | catchError(error => { 51 | this.setAuthState({id: null, token: null, isAuthenticated: false}); 52 | return throwError(error); 53 | }) 54 | ); 55 | } 56 | 57 | signupUser(variables: {name: string, email: string, password: string}): Observable<{id: string, token: string}> { 58 | return this.apollo.mutate({ 59 | mutation: SIGNUP_USER_MUTATION, 60 | variables 61 | }).pipe( 62 | map(res => res.data.signupUser), 63 | tap(res => this.setAuthState({id: res && res.id, token: res && res.token, isAuthenticated: res !== null})), 64 | catchError(error => { 65 | this.setAuthState({id: null, token: null, isAuthenticated: false}); 66 | return throwError(error); 67 | }) 68 | ); 69 | } 70 | 71 | toggleKeepSigned(): void { 72 | this.keepSigned = !this.keepSigned; 73 | window.localStorage.setItem(StorageKeys.KEEP_SIGNED, this.keepSigned.toString()); 74 | } 75 | 76 | toggleRememberMe(): void { 77 | this.rememberMe = !this.rememberMe; 78 | window.localStorage.setItem(StorageKeys.REMEMBER_ME, this.rememberMe.toString()); 79 | if (!this.rememberMe) { 80 | window.localStorage.removeItem(StorageKeys.USER_EMAIL); 81 | window.localStorage.removeItem(StorageKeys.USER_PASSWORD); 82 | } 83 | } 84 | 85 | setRememberMe(user: { email: string, password: string }): void { 86 | if (this.rememberMe) { 87 | window.localStorage.setItem(StorageKeys.USER_EMAIL, Base64.encode(user.email)); 88 | window.localStorage.setItem(StorageKeys.USER_PASSWORD, Base64.encode(user.password)); 89 | } 90 | } 91 | 92 | getRememberMe(): { email: string, password: string } { 93 | if (!this.rememberMe) { return null; } 94 | return { 95 | email: Base64.decode(window.localStorage.getItem(StorageKeys.USER_EMAIL)), 96 | password: Base64.decode(window.localStorage.getItem(StorageKeys.USER_PASSWORD)) 97 | }; 98 | } 99 | 100 | logout(): void { 101 | this.apolloConfigModule.closeWebSocketConnection(); 102 | window.localStorage.removeItem(StorageKeys.AUTH_TOKEN); 103 | window.localStorage.removeItem(StorageKeys.KEEP_SIGNED); 104 | this.apolloConfigModule.cachePersistor.purge(); 105 | this.keepSigned = false; 106 | this._isAuthenticated.next(false); 107 | this.router.navigate(['/login']); 108 | this.apollo.getClient().resetStore(); 109 | } 110 | 111 | autoLogin(): Observable { 112 | if (!this.keepSigned) { 113 | this._isAuthenticated.next(false); 114 | window.localStorage.removeItem(StorageKeys.AUTH_TOKEN); 115 | return of(); 116 | } 117 | 118 | return this.validateToken() 119 | .pipe( 120 | tap(authData => { 121 | const token = window.localStorage.getItem(StorageKeys.AUTH_TOKEN); 122 | this.setAuthState({id: authData.id, token, isAuthenticated: authData.isAuthenticated}, true); 123 | }), 124 | mergeMap(res => of()), 125 | catchError(error => { 126 | this.setAuthState({id: null, token: null, isAuthenticated: false}); 127 | return throwError(error); 128 | }) 129 | ); 130 | } 131 | 132 | private validateToken(): Observable<{id: string, isAuthenticated: boolean}> { 133 | return this.apollo.query({ 134 | query: LOGGED_IN_USER_QUERY, 135 | fetchPolicy: 'network-only' 136 | }).pipe( 137 | map(res => { 138 | const user = res.data.loggedInUser; 139 | return { 140 | id: user && user.id, 141 | isAuthenticated: user !== null 142 | }; 143 | }), 144 | mergeMap(authData => (authData.isAuthenticated) ? of(authData) : throwError(new Error('Invalid token!'))) 145 | ); 146 | } 147 | 148 | private setAuthUser(userId: string): Observable { 149 | return this.userService.getUserById(userId) 150 | .pipe( 151 | tap((user: User) => this.authUser = user) 152 | ); 153 | } 154 | 155 | private setAuthState(authData: {id: string, token: string, isAuthenticated: boolean}, isRefresh: boolean = false): void { 156 | if (authData.isAuthenticated) { 157 | window.localStorage.setItem(StorageKeys.AUTH_TOKEN, authData.token); 158 | this.setAuthUser(authData.id) 159 | .pipe( 160 | take(1), 161 | tap(() => this._isAuthenticated.next(authData.isAuthenticated)) 162 | ).subscribe(); 163 | if (!isRefresh) { 164 | this.apolloConfigModule.closeWebSocketConnection(); 165 | } 166 | return; 167 | } 168 | this._isAuthenticated.next(authData.isAuthenticated); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/app/core/services/base.service.ts: -------------------------------------------------------------------------------- 1 | import { DataProxy } from 'apollo-cache'; 2 | import { DocumentNode } from 'graphql'; 3 | 4 | export abstract class BaseService { 5 | 6 | protected readAndWriteQuery( 7 | config: { 8 | store: DataProxy, 9 | newRecord: T, 10 | query: DocumentNode, 11 | queryName: string, 12 | arrayOperation: 'push' | 'unshift' | 'singleRecord', 13 | variables?: { [key: string]: any } 14 | } 15 | ): void { 16 | 17 | try { 18 | 19 | const data = config.store.readQuery({ 20 | query: config.query, 21 | variables: config.variables 22 | }); 23 | 24 | switch (config.arrayOperation) { 25 | case 'push': 26 | case 'unshift': 27 | data[config.queryName] = [...data[config.queryName]]; 28 | data[config.queryName][config.arrayOperation](config.newRecord); 29 | break; 30 | case 'singleRecord': 31 | data[config.queryName] = [config.newRecord]; 32 | } 33 | 34 | config.store.writeQuery({ 35 | query: config.query, 36 | variables: config.variables, 37 | data 38 | }); 39 | 40 | } catch (e) { 41 | console.log(`Query ${config.queryName} not found in cache!`); 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/services/error.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ErrorService { 7 | 8 | constructor() { } 9 | 10 | getErrorMessage(error: Error): string { 11 | const message = error.message.split(': '); 12 | return message[message.length - 1]; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/services/file.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const FileFragment = gql` 4 | fragment FileFragment on File { 5 | id 6 | secret 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /src/app/core/services/file.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { FileModel } from '../models/file.model'; 6 | import { GRAPHCOOL_CONFIG, GraphcoolConfig } from '../providers/graphcool-config.provider'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class FileService { 12 | 13 | constructor( 14 | @Inject(GRAPHCOOL_CONFIG) private graphcoolConfig: GraphcoolConfig, 15 | private http: HttpClient 16 | ) { } 17 | 18 | upload(file: File): Observable { 19 | const formData = new FormData(); 20 | formData.append('data', file); 21 | return this.http.post(this.graphcoolConfig.fileAPI, formData); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/services/user.graphql.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from 'graphql'; 2 | import gql from 'graphql-tag'; 3 | 4 | import { FileFragment } from './file.graphql'; 5 | import { User } from '../models/user.model'; 6 | 7 | export interface AllUsersQuery { 8 | allUsers: User[]; 9 | } 10 | 11 | export interface UserQuery { 12 | User: User; 13 | } 14 | 15 | const UserFragment = gql` 16 | fragment UserFragment on User { 17 | id 18 | name 19 | email 20 | createdAt 21 | photo { 22 | ...FileFragment 23 | } 24 | } 25 | ${FileFragment} 26 | `; 27 | 28 | export const ALL_USERS_QUERY = gql` 29 | query AllUsersQuery($idToExclude: ID!) { 30 | allUsers( 31 | orderBy: name_ASC, 32 | filter: { 33 | id_not: $idToExclude 34 | } 35 | ) { 36 | ...UserFragment 37 | } 38 | } 39 | ${UserFragment} 40 | `; 41 | 42 | export const GET_USER_BY_ID_QUERY = gql` 43 | query GetUserByIdQuery($userId: ID!) { 44 | User(id: $userId) { 45 | ...UserFragment 46 | } 47 | } 48 | ${UserFragment} 49 | `; 50 | 51 | export const UPDATE_USER_MUTATION = gql` 52 | mutation UpdateUserMutation($id: ID!, $name: String!, $email: String!) { 53 | updateUser( 54 | id: $id, 55 | name: $name, 56 | email: $email 57 | ) { 58 | ...UserFragment 59 | } 60 | } 61 | ${UserFragment} 62 | `; 63 | 64 | 65 | const updateUserPhotoMutation = ` 66 | updateUser(id: $loggedUserId, photoId: $newPhotoId, dummy: "dummy") { 67 | ...UserFragment 68 | } 69 | `; 70 | 71 | const deleteFileMutation = ` 72 | deleteFile(id: $oldPhotoId) { 73 | id 74 | secret 75 | } 76 | `; 77 | 78 | export const getUpdateUserPhotoMutation = (hasOldPhoto: boolean): DocumentNode => { 79 | if (hasOldPhoto) { 80 | return gql` 81 | mutation UpdateAndDeleteUserPhoto($loggedUserId: ID!, $newPhotoId: ID!, $oldPhotoId: ID!) { 82 | ${updateUserPhotoMutation} 83 | ${deleteFileMutation} 84 | } 85 | ${UserFragment} 86 | `; 87 | } 88 | return gql` 89 | mutation UpdateUserPhoto($loggedUserId: ID!, $newPhotoId: ID!) { 90 | ${updateUserPhotoMutation} 91 | } 92 | ${UserFragment} 93 | `; 94 | }; 95 | 96 | 97 | export const USERS_SUBSCRIPTION = gql` 98 | subscription UsersSubscription { 99 | User( 100 | filter: { 101 | mutation_in: [ CREATED, UPDATED ] 102 | } 103 | ) { 104 | mutation 105 | node { 106 | ...UserFragment 107 | } 108 | } 109 | } 110 | ${UserFragment} 111 | `; 112 | -------------------------------------------------------------------------------- /src/app/core/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Apollo, QueryRef } from 'apollo-angular'; 3 | import { Observable, Subscription } from 'rxjs'; 4 | import { map, mergeMap } from 'rxjs/operators'; 5 | 6 | import { FileModel } from '../models/file.model'; 7 | import { FileService } from './file.service'; 8 | import { User } from '../models/user.model'; 9 | import { 10 | ALL_USERS_QUERY, 11 | GET_USER_BY_ID_QUERY, 12 | USERS_SUBSCRIPTION, 13 | UPDATE_USER_MUTATION, 14 | AllUsersQuery, 15 | UserQuery, 16 | getUpdateUserPhotoMutation 17 | } from './user.graphql'; 18 | 19 | @Injectable({ 20 | providedIn: 'root' 21 | }) 22 | export class UserService { 23 | 24 | users$: Observable; 25 | private queryRef: QueryRef; 26 | private usersSubscription: Subscription; 27 | 28 | constructor( 29 | private apollo: Apollo, 30 | private fileService: FileService 31 | ) { } 32 | 33 | startUsersMonitoring(idToExclude: string): void { 34 | if (!this.users$) { 35 | this.users$ = this.allUsers(idToExclude); 36 | this.usersSubscription = this.users$.subscribe(); 37 | } 38 | } 39 | 40 | stopUsersMonitoring(): void { 41 | if (this.usersSubscription) { 42 | this.usersSubscription.unsubscribe(); 43 | this.usersSubscription = null; 44 | this.users$ = null; 45 | } 46 | } 47 | 48 | allUsers(idToExclude: string): Observable { 49 | this.queryRef = this.apollo 50 | .watchQuery({ 51 | query: ALL_USERS_QUERY, 52 | variables: { 53 | idToExclude 54 | }, 55 | fetchPolicy: 'network-only' 56 | }); 57 | 58 | this.queryRef.subscribeToMore({ 59 | document: USERS_SUBSCRIPTION, 60 | updateQuery: (previous: AllUsersQuery, { subscriptionData }): AllUsersQuery => { 61 | 62 | const subscriptionUser: User = subscriptionData.data.User.node; 63 | const newAllUsers: User[] = [ ...previous.allUsers ]; 64 | 65 | switch (subscriptionData.data.User.mutation) { 66 | case 'CREATED': 67 | newAllUsers.unshift(subscriptionUser); 68 | break; 69 | case 'UPDATED': 70 | const userToUpdateIndex: number = newAllUsers.findIndex(u => u.id === subscriptionUser.id); 71 | if (userToUpdateIndex > -1) { 72 | newAllUsers[userToUpdateIndex] = subscriptionUser; 73 | } 74 | } 75 | 76 | return { 77 | ...previous, 78 | allUsers: newAllUsers.sort((uA, uB) => { 79 | if (uA.name < uB.name) { return -1; } 80 | if (uA.name > uB.name) { return 1; } 81 | return 0; 82 | }) 83 | }; 84 | } 85 | }); 86 | 87 | return this.queryRef.valueChanges 88 | .pipe( 89 | map(res => res.data.allUsers), 90 | map(users => users.map(user => new User(user))) 91 | ); 92 | } 93 | 94 | getUserById(id: string): Observable { 95 | return this.apollo 96 | .query({ 97 | query: GET_USER_BY_ID_QUERY, 98 | variables: { userId: id } 99 | }).pipe( 100 | map(res => res.data.User), 101 | map(user => new User(user)) 102 | ); 103 | } 104 | 105 | updateUser(user: User): Observable { 106 | return this.apollo.mutate({ 107 | mutation: UPDATE_USER_MUTATION, 108 | variables: { 109 | id: user.id, 110 | name: user.name, 111 | email: user.email 112 | } 113 | }).pipe( 114 | map(res => res.data.updateUser) 115 | ); 116 | } 117 | 118 | updateUserPhoto(file: File, user: User): Observable { 119 | return this.fileService.upload(file) 120 | .pipe( 121 | mergeMap((newPhoto: FileModel) => { 122 | return this.apollo.mutate({ 123 | mutation: getUpdateUserPhotoMutation(!!user.photo), 124 | variables: { 125 | loggedUserId: user.id, 126 | newPhotoId: newPhoto.id, 127 | oldPhotoId: (user.photo) ? user.photo.id : null 128 | } 129 | }).pipe( 130 | map(res => res.data.updateUser) 131 | ); 132 | }) 133 | ); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/app/core/strategy/authenticated-preloading.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PreloadingStrategy, Route } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { mergeMap } from 'rxjs/operators'; 5 | 6 | import { AuthService } from '../services/auth.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthenticatedPreloadingStrategy implements PreloadingStrategy { 12 | 13 | constructor( 14 | private authService: AuthService 15 | ) {} 16 | 17 | preload(route: Route, load: () => Observable): Observable { 18 | return this.authService.isAuthenticated 19 | .pipe( 20 | mergeMap(is => is ? load() : of(null)) 21 | ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/strategy/selective-preloading.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PreloadingStrategy, Route } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SelectivePreloadingStrategy implements PreloadingStrategy { 9 | 10 | preload(route: Route, load: () => Observable): Observable { 11 | return route.data && route.data.preload ? load() : of(null); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-header/dashboard-header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 |

{{ title.getTitle() }}

9 | 10 | 11 | 12 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-header/dashboard-header.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/app/dashboard/components/dashboard-header/dashboard-header.component.scss -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-header/dashboard-header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardHeaderComponent } from './dashboard-header.component'; 4 | 5 | describe('DashboardHeaderComponent', () => { 6 | let component: DashboardHeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardHeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardHeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-header/dashboard-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | import { MatDialog, MatSidenav } from '@angular/material'; 4 | 5 | import { AuthService } from '../../../core/services/auth.service'; 6 | import { BaseComponent } from '../../../shared/components/base.component'; 7 | 8 | @Component({ 9 | selector: 'app-dashboard-header', 10 | templateUrl: './dashboard-header.component.html', 11 | styleUrls: ['./dashboard-header.component.scss'] 12 | }) 13 | export class DashboardHeaderComponent extends BaseComponent { 14 | 15 | @Input() sidenav: MatSidenav; 16 | 17 | constructor( 18 | authService: AuthService, 19 | dialog: MatDialog, 20 | public title: Title 21 | ) { 22 | super(authService, dialog); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-home/dashboard-home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Menu 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | arrow_forward 17 |

Logout

18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-home/dashboard-home.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100vh; 3 | width: 100vw; 4 | } 5 | 6 | .sidenav { 7 | width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-home/dashboard-home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardHomeComponent } from './dashboard-home.component'; 4 | 5 | describe('DashboardHomeComponent', () => { 6 | let component: DashboardHomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardHomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardHomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-home/dashboard-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatDialog, MatSidenav } from '@angular/material'; 3 | 4 | import { AuthService } from '../../../core/services/auth.service'; 5 | import { BaseComponent } from '../../../shared/components/base.component'; 6 | 7 | @Component({ 8 | selector: 'app-dashboard-home', 9 | templateUrl: './dashboard-home.component.html', 10 | styleUrls: ['./dashboard-home.component.scss'] 11 | }) 12 | export class DashboardHomeComponent extends BaseComponent { 13 | 14 | constructor( 15 | authService: AuthService, 16 | dialog: MatDialog 17 | ) { 18 | super(authService, dialog); 19 | } 20 | 21 | onLogout(sidenav: MatSidenav): void { 22 | sidenav.close(); 23 | this.logout(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-permission-denied/dashboard-permission-denied.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardPermissionDeniedComponent } from './dashboard-permission-denied.component'; 4 | 5 | describe('DashboardPermissionDeniedComponent', () => { 6 | let component: DashboardPermissionDeniedComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardPermissionDeniedComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardPermissionDeniedComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-permission-denied/dashboard-permission-denied.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { take } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard-permission-denied', 7 | template: ` 8 | 9 |

Permission denied!

10 |

You tried to access a page that does not have sufficient permissions!

11 |

Feature blocked: "{{ featureBlocked }}"

12 | Back to home 13 |
14 | `, 15 | styles: [] 16 | }) 17 | export class DashboardPermissionDeniedComponent implements OnInit { 18 | 19 | featureBlocked: string; 20 | 21 | constructor( 22 | private route: ActivatedRoute 23 | ) { } 24 | 25 | ngOnInit(): void { 26 | this.route.queryParamMap 27 | .pipe(take(1)) 28 | .subscribe(paramMap => this.featureBlocked = paramMap.get('previous')); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-resources/dashboard-resources.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardResourcesComponent } from './dashboard-resources.component'; 4 | 5 | describe('DashboardResourcesComponent', () => { 6 | let component: DashboardResourcesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardResourcesComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardResourcesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/components/dashboard-resources/dashboard-resources.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard-resources', 5 | template: ` 6 | 7 | 8 | 13 | {{ link.icon }} 14 |

{{ link.title }}

15 |
16 | 17 | 18 | 19 |
20 | ` 21 | }) 22 | export class DashboardResourcesComponent implements OnInit { 23 | 24 | @Input() isMenu = false; 25 | @Output() close = new EventEmitter(); 26 | 27 | resources: any[] = [ 28 | { 29 | url: '/dashboard/chat', 30 | icon: 'chat_bubble', 31 | title: 'My Chats' 32 | }, 33 | { 34 | url: '/dashboard/chat/users', 35 | icon: 'people', 36 | title: 'All Users' 37 | }, 38 | { 39 | url: '/dashboard/profile', 40 | icon: 'person', 41 | title: 'Profile' 42 | } 43 | ]; 44 | 45 | ngOnInit(): void { 46 | if (this.isMenu) { 47 | this.resources.unshift({ 48 | url: '/dashboard', 49 | icon: 'home', 50 | title: 'Home' 51 | }); 52 | } 53 | } 54 | 55 | onClose(): void { 56 | this.close.emit(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '../login/auth.guard'; 5 | import { DashboardHomeComponent } from './components/dashboard-home/dashboard-home.component'; 6 | import { DashboardPermissionDeniedComponent } from './components/dashboard-permission-denied/dashboard-permission-denied.component'; 7 | import { DashboardResourcesComponent } from './components/dashboard-resources/dashboard-resources.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: DashboardHomeComponent, 13 | canActivate: [ AuthGuard ], 14 | canActivateChild: [ AuthGuard ], 15 | children: [ 16 | { path: 'chat', loadChildren: './../chat/chat.module#ChatModule', canActivate: [ AuthGuard ] }, 17 | { path: 'profile', loadChildren: './../user/user.module#UserModule', canActivate: [ AuthGuard ] }, 18 | { path: 'permission-denied', component: DashboardPermissionDeniedComponent, canActivate: [ AuthGuard ] }, 19 | { path: '', component: DashboardResourcesComponent } 20 | ] 21 | } 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forChild(routes)], 26 | exports: [RouterModule] 27 | }) 28 | export class DashboardRoutingModule { } 29 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { DashboardHeaderComponent } from './components/dashboard-header/dashboard-header.component'; 4 | import { DashboardHomeComponent } from './components/dashboard-home/dashboard-home.component'; 5 | import { DashboardRoutingModule } from './dashboard-routing.module'; 6 | import { SharedModule } from '../shared/shared.module'; 7 | import { DashboardPermissionDeniedComponent } from './components/dashboard-permission-denied/dashboard-permission-denied.component'; 8 | import { DashboardResourcesComponent } from './components/dashboard-resources/dashboard-resources.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | SharedModule, 13 | DashboardRoutingModule 14 | ], 15 | declarations: [ 16 | DashboardHomeComponent, 17 | DashboardHeaderComponent, 18 | DashboardPermissionDeniedComponent, 19 | DashboardResourcesComponent 20 | ] 21 | }) 22 | export class DashboardModule { } 23 | -------------------------------------------------------------------------------- /src/app/login/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | CanActivateChild, 6 | CanLoad, 7 | Route, 8 | Router, 9 | RouterStateSnapshot 10 | } from '@angular/router'; 11 | import { Observable } from 'rxjs'; 12 | import { tap, take } from 'rxjs/operators'; 13 | 14 | import { AuthService } from '../core/services/auth.service'; 15 | import { LoginRoutingModule } from './login-routing.module'; 16 | 17 | @Injectable({ 18 | providedIn: LoginRoutingModule 19 | }) 20 | export class AuthGuard implements CanActivate, CanActivateChild, CanLoad { 21 | 22 | constructor( 23 | private authService: AuthService, 24 | private router: Router 25 | ) { } 26 | 27 | canActivate( 28 | route: ActivatedRouteSnapshot, 29 | state: RouterStateSnapshot 30 | ): Observable { 31 | return this.checkAuthState(state.url); 32 | } 33 | 34 | canActivateChild( 35 | route: ActivatedRouteSnapshot, 36 | state: RouterStateSnapshot 37 | ): Observable { 38 | return this.canActivate(route, state); 39 | } 40 | 41 | canLoad(route: Route): Observable { 42 | // route.path 43 | const url = window.location.pathname; 44 | return this.checkAuthState(url) 45 | .pipe(take(1)); 46 | } 47 | 48 | private checkAuthState(url: string): Observable { 49 | return this.authService.isAuthenticated 50 | .pipe( 51 | tap(is => { 52 | if (!is) { 53 | this.authService.redirectUrl = url; 54 | this.router.navigate(['/login']); 55 | } 56 | }) 57 | ); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/app/login/auto-login.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { map, tap } from 'rxjs/operators'; 5 | 6 | import { AuthService } from '../core/services/auth.service'; 7 | 8 | @Injectable() 9 | export class AutoLoginGuard implements CanActivate { 10 | 11 | constructor( 12 | private authService: AuthService, 13 | private router: Router 14 | ) { } 15 | 16 | canActivate( 17 | route: ActivatedRouteSnapshot, 18 | state: RouterStateSnapshot 19 | ): Observable { 20 | return this.authService.isAuthenticated 21 | .pipe( 22 | tap(is => (is) ? this.router.navigate(['/dashboard']) : null), 23 | map(is => !is) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/login/components/login/login.component.html: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /src/app/login/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | height: 90vh; 3 | display: flex; 4 | justify-content: center; 5 | align-content: center; 6 | align-items: center; 7 | } 8 | 9 | mat-card { 10 | width: 280px; 11 | } 12 | 13 | form { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/login/components/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/login/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { MatSnackBar } from '@angular/material'; 4 | import { Router } from '@angular/router'; 5 | import { takeWhile } from 'rxjs/operators'; 6 | 7 | import { AuthService } from '../../../core/services/auth.service'; 8 | import { ErrorService } from '../../../core/services/error.service'; 9 | 10 | @Component({ 11 | templateUrl: './login.component.html', 12 | styleUrls: ['./login.component.scss'] 13 | }) 14 | export class LoginComponent implements OnInit, OnDestroy { 15 | 16 | loginForm: FormGroup; 17 | configs = { 18 | isLogin: true, 19 | actionText: 'SignIn', 20 | buttonActionText: 'Create account', 21 | isLoading: false 22 | }; 23 | private nameControl = 24 | new FormControl('', [Validators.required, Validators.minLength(5)]); 25 | private alive = true; 26 | 27 | @HostBinding('class.app-login-spinner') private applySpinnerClass = true; 28 | 29 | constructor( 30 | public authService: AuthService, 31 | private errorService: ErrorService, 32 | private formBuilder: FormBuilder, 33 | private router: Router, 34 | private snackBar: MatSnackBar 35 | ) { } 36 | 37 | ngOnInit() { 38 | this.createForm(); 39 | 40 | const userData = this.authService.getRememberMe(); 41 | if (userData) { 42 | this.email.setValue(userData.email); 43 | this.password.setValue(userData.password); 44 | } 45 | } 46 | 47 | createForm(): void { 48 | this.loginForm = this.formBuilder.group({ 49 | email: ['', [Validators.required, Validators.email]], 50 | password: ['', [Validators.required, Validators.minLength(5)]] 51 | }); 52 | } 53 | 54 | onSubmit(): void { 55 | 56 | this.configs.isLoading = true; 57 | 58 | const operation = 59 | (this.configs.isLogin) 60 | ? this.authService.signinUser(this.loginForm.value) 61 | : this.authService.signupUser(this.loginForm.value); 62 | 63 | operation 64 | .pipe( 65 | takeWhile(() => this.alive) 66 | ).subscribe( 67 | res => { 68 | this.authService.setRememberMe(this.loginForm.value); 69 | const redirect: string = this.authService.redirectUrl || '/dashboard'; 70 | 71 | this.authService.isAuthenticated 72 | .pipe(takeWhile(() => this.alive)) 73 | .subscribe((is: boolean) => { 74 | if (is) { 75 | this.router.navigate([redirect]); 76 | this.authService.redirectUrl = null; 77 | this.configs.isLoading = false; 78 | } 79 | }); 80 | }, 81 | err => { 82 | console.log(err); 83 | this.configs.isLoading = false; 84 | this.snackBar.open(this.errorService.getErrorMessage(err), 'Done', {duration: 5000, verticalPosition: 'top'}); 85 | } 86 | ); 87 | 88 | } 89 | 90 | changeAction(): void { 91 | this.configs.isLogin = !this.configs.isLogin; 92 | this.configs.actionText = !this.configs.isLogin ? 'SignUp' : 'SignIn'; 93 | this.configs.buttonActionText = !this.configs.isLogin ? 'Already have account' : 'Create account'; 94 | !this.configs.isLogin ? this.loginForm.addControl('name', this.nameControl) : this.loginForm.removeControl('name'); 95 | } 96 | 97 | get name(): FormControl { return this.loginForm.get('name'); } 98 | get email(): FormControl { return this.loginForm.get('email'); } 99 | get password(): FormControl { return this.loginForm.get('password'); } 100 | 101 | onKeepSigned(): void { 102 | this.authService.toggleKeepSigned(); 103 | } 104 | 105 | onRememberMe(): void { 106 | this.authService.toggleRememberMe(); 107 | } 108 | 109 | ngOnDestroy(): void { 110 | this.alive = false; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AutoLoginGuard } from './auto-login.guard'; 5 | import { LoginComponent } from './components/login/login.component'; 6 | 7 | const routes: Routes = [ 8 | { path: 'login', component: LoginComponent, canActivate: [ AutoLoginGuard ] } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule], 14 | providers: [ AutoLoginGuard ] 15 | }) 16 | export class LoginRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { LoginComponent } from './components/login/login.component'; 4 | import { LoginRoutingModule } from './login-routing.module'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | LoginRoutingModule 11 | ], 12 | declarations: [LoginComponent], 13 | exports: [LoginComponent] 14 | }) 15 | export class LoginModule { } 16 | -------------------------------------------------------------------------------- /src/app/shared/components/avatar/avatar.component.scss: -------------------------------------------------------------------------------- 1 | .avatar-container { 2 | position: relative; 3 | width: 180px; 4 | height: 180px; 5 | margin: 0 auto; 6 | border-radius: 100%; 7 | img { 8 | border-radius: 100%; 9 | max-height: 100%; 10 | margin: auto; 11 | display: block; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/components/avatar/avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-avatar', 5 | template: ` 6 |
7 | 8 | 9 |
10 | `, 11 | styleUrls: ['./avatar.component.scss'] 12 | }) 13 | export class AvatarComponent { 14 | @Input() src: string; 15 | @Input() title: string; 16 | @Input() imageStyles: {[key: string]: string | number} = {}; 17 | @Input() containerStyles: {[key: string]: string | number} = {}; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/components/base.component.ts: -------------------------------------------------------------------------------- 1 | import { MatDialog } from '@angular/material'; 2 | import { take } from 'rxjs/operators'; 3 | 4 | import { AuthService } from '../../core/services/auth.service'; 5 | import { DialogConfirmComponent } from './dialog-confirm/dialog-confirm.component'; 6 | import { DialogConfirmData } from './dialog-confirm/dialog-confirm-data.interface'; 7 | 8 | export class BaseComponent { 9 | 10 | constructor( 11 | protected authService?: AuthService, 12 | protected dialog?: MatDialog 13 | ) {} 14 | 15 | trackByFn(index: number, item: T): string { 16 | return item.id; 17 | } 18 | 19 | logout(): void { 20 | const dialogRef = this.dialog.open( 21 | DialogConfirmComponent, 22 | { data: { title: 'Quit?', message: 'Do you really want to leave?' } } 23 | ); 24 | 25 | dialogRef.beforeClose() 26 | .pipe(take(1)) 27 | .subscribe((confirmed: boolean) => { 28 | if (confirmed) { 29 | this.authService.logout(); 30 | } 31 | }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/components/dialog-confirm/dialog-confirm-data.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DialogConfirmData { 2 | title: string; 3 | message: string; 4 | disableClose?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/components/dialog-confirm/dialog-confirm.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DialogConfirmComponent } from './dialog-confirm.component'; 4 | 5 | describe('DialogConfirmComponent', () => { 6 | let component: DialogConfirmComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DialogConfirmComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DialogConfirmComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/dialog-confirm/dialog-confirm.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; 3 | 4 | import { DialogConfirmData } from './dialog-confirm-data.interface'; 5 | 6 | @Component({ 7 | selector: 'app-dialog-confirm', 8 | template: ` 9 |

{{ data.title }}

10 | 11 | {{ data.message }} 12 | 13 | 14 | 15 | 16 | 17 | `, 18 | styles: [] 19 | }) 20 | export class DialogConfirmComponent implements OnInit { 21 | 22 | constructor( 23 | @Inject(MAT_DIALOG_DATA) public data: DialogConfirmData, 24 | private dialogRef: MatDialogRef 25 | ) { } 26 | 27 | ngOnInit(): void { 28 | this.dialogRef.disableClose = (this.data.disableClose !== undefined) ? this.data.disableClose : true; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/components/image-preview/image-preview.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

No image selected.

6 |
7 |
8 |
9 | 10 | 11 |

{{ selectedImage ? selectedImage.name : 'No photo' }}

12 | 13 | 16 | 19 |
20 | -------------------------------------------------------------------------------- /src/app/shared/components/image-preview/image-preview.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ImagePreviewComponent } from './image-preview.component'; 4 | 5 | describe('ImagePreviewComponent', () => { 6 | let component: ImagePreviewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ImagePreviewComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ImagePreviewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/image-preview/image-preview.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; 3 | 4 | @Component({ 5 | selector: 'app-image-preview', 6 | templateUrl: './image-preview.component.html', 7 | styles: [ 8 | ` 9 | img { 10 | max-width: 100%; 11 | margin: auto; 12 | display: block; 13 | } 14 | 15 | img[mat-card-image] { 16 | margin-top: 0; 17 | } 18 | 19 | mat-card { 20 | padding: 0; 21 | } 22 | ` 23 | ] 24 | }) 25 | export class ImagePreviewComponent implements OnInit { 26 | 27 | selectedImage: File; 28 | 29 | constructor( 30 | @Inject(MAT_DIALOG_DATA) private data: { image: File }, 31 | private dialogRef: MatDialogRef 32 | ) { } 33 | 34 | ngOnInit(): void { 35 | this.selectedImage = this.data.image; 36 | } 37 | 38 | onSave(): void { 39 | this.dialogRef.close({ 40 | canSave: true, 41 | selectedImage: this.selectedImage 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/app/shared/components/no-record/no-record.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 30px; 3 | display: flex; 4 | align-items: center; 5 | flex-direction: column; 6 | } 7 | 8 | h3, mat-icon { 9 | color: #ccc; 10 | } 11 | 12 | mat-icon { 13 | width: 150px; 14 | height: 150px; 15 | font-size: 150px; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/components/no-record/no-record.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-no-record', 5 | template: ` 6 |
7 | {{ icon }} 8 |

{{ title }}

9 |
10 | `, 11 | styleUrls: ['./no-record.component.scss'] 12 | }) 13 | export class NoRecordComponent { 14 | @Input() icon: string; 15 | @Input() title: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/components/warning/warning.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { WarningComponent } from './warning.component'; 4 | 5 | describe('WarningComponent', () => { 6 | let component: WarningComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ WarningComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(WarningComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/warning/warning.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-warning', 5 | template: ` 6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 | `, 14 | styles: [ 15 | ` 16 | .warning-container { 17 | display: flex; 18 | justify-content: center; 19 | align-content: center; 20 | align-items: center; 21 | flex-direction: column; 22 | } 23 | 24 | mat-card { 25 | margin-top: 20px; 26 | width: 80vw; 27 | text-align: center; 28 | } 29 | ` 30 | ] 31 | }) 32 | export class WarningComponent {} 33 | -------------------------------------------------------------------------------- /src/app/shared/pipes/from-now.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { FromNowPipe } from './from-now.pipe'; 2 | 3 | describe('FromNowPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new FromNowPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/pipes/from-now.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { AppConfigService } from '../../core/services/app-config.service'; 4 | 5 | @Pipe({ 6 | name: 'fromNow', 7 | pure: false 8 | }) 9 | export class FromNowPipe implements PipeTransform { 10 | 11 | constructor( 12 | private appConfig: AppConfigService 13 | ) {} 14 | 15 | transform(date: string): string { 16 | return timeDifferenceForDate(date, this.appConfig.timeDifference); 17 | } 18 | 19 | } 20 | 21 | function getTimeDifference(current: number, previous: number) { 22 | const milliSecondsPerMinute = 60 * 1000; 23 | const milliSecondsPerHour = milliSecondsPerMinute * 60; 24 | const milliSecondsPerDay = milliSecondsPerHour * 24; 25 | const milliSecondsPerMonth = milliSecondsPerDay * 30; 26 | const milliSecondsPerYear = milliSecondsPerDay * 365; 27 | 28 | const elapsed = current - previous; 29 | 30 | if (elapsed < milliSecondsPerMinute / 3) { 31 | return 'just now'; 32 | } 33 | 34 | if (elapsed < milliSecondsPerMinute) { 35 | return 'less than 1 min ago'; 36 | } else if (elapsed < milliSecondsPerHour) { 37 | return Math.round(elapsed / milliSecondsPerMinute) + ' min ago'; 38 | } else if (elapsed < milliSecondsPerDay) { 39 | return Math.round(elapsed / milliSecondsPerHour) + ' h ago'; 40 | } else if (elapsed < milliSecondsPerMonth) { 41 | return Math.round(elapsed / milliSecondsPerDay) + ' days ago'; 42 | } else if (elapsed < milliSecondsPerYear) { 43 | return Math.round(elapsed / milliSecondsPerMonth) + ' mo ago'; 44 | } else { 45 | return Math.round(elapsed / milliSecondsPerYear) + ' years ago'; 46 | } 47 | } 48 | 49 | function timeDifferenceForDate(date: string, timeDifference: number) { 50 | const now = new Date().getTime() + timeDifference; 51 | const updated = new Date(date).getTime(); 52 | return getTimeDifference(now, updated); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/shared/pipes/read-file.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReadFilePipe } from './read-file.pipe'; 2 | 3 | describe('ReadFilePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new ReadFilePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/shared/pipes/read-file.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { Observable, Observer, of } from 'rxjs'; 3 | 4 | @Pipe({ 5 | name: 'readFile' 6 | }) 7 | export class ReadFilePipe implements PipeTransform { 8 | 9 | transform(file: File): Observable { 10 | if (file) { 11 | const fileReader = new FileReader(); 12 | fileReader.readAsDataURL(file); 13 | 14 | return Observable.create((observer: Observer) => { 15 | fileReader.onloadend = (event: ProgressEvent) => { 16 | observer.next(fileReader.result as string); 17 | observer.complete(); 18 | }; 19 | fileReader.onerror = (event) => { 20 | observer.error(event); 21 | observer.complete(); 22 | }; 23 | }); 24 | } 25 | return of(null); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { SharedModule } from './shared.module'; 2 | 3 | describe('SharedModule', () => { 4 | let sharedModule: SharedModule; 5 | 6 | beforeEach(() => { 7 | sharedModule = new SharedModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(sharedModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { 5 | MatButtonModule, 6 | MatCardModule, 7 | MatDialogModule, 8 | MatFormFieldModule, 9 | MatIconModule, 10 | MatInputModule, 11 | MatLineModule, 12 | MatListModule, 13 | MatMenuModule, 14 | MatProgressSpinnerModule, 15 | MatSidenavModule, 16 | MatSnackBarModule, 17 | MatSlideToggleModule, 18 | MatTabsModule, 19 | MatToolbarModule 20 | } from '@angular/material'; 21 | 22 | import { AvatarComponent } from './components/avatar/avatar.component'; 23 | import { DialogConfirmComponent } from './components/dialog-confirm/dialog-confirm.component'; 24 | import { FromNowPipe } from './pipes/from-now.pipe'; 25 | import { ImagePreviewComponent } from './components/image-preview/image-preview.component'; 26 | import { NoRecordComponent } from './components/no-record/no-record.component'; 27 | import { ReadFilePipe } from './pipes/read-file.pipe'; 28 | import { WarningComponent } from './components/warning/warning.component'; 29 | 30 | @NgModule({ 31 | declarations: [ 32 | AvatarComponent, 33 | DialogConfirmComponent, 34 | FromNowPipe, 35 | ImagePreviewComponent, 36 | NoRecordComponent, 37 | ReadFilePipe, 38 | WarningComponent 39 | ], 40 | imports: [ 41 | CommonModule, 42 | MatButtonModule, 43 | MatCardModule, 44 | MatDialogModule, 45 | MatIconModule, 46 | MatToolbarModule 47 | ], 48 | entryComponents: [ 49 | DialogConfirmComponent, 50 | ImagePreviewComponent 51 | ], 52 | exports: [ 53 | AvatarComponent, 54 | CommonModule, 55 | DialogConfirmComponent, 56 | FormsModule, 57 | FromNowPipe, 58 | ImagePreviewComponent, 59 | MatButtonModule, 60 | MatCardModule, 61 | MatDialogModule, 62 | MatFormFieldModule, 63 | MatIconModule, 64 | MatInputModule, 65 | MatLineModule, 66 | MatListModule, 67 | MatMenuModule, 68 | MatProgressSpinnerModule, 69 | MatSidenavModule, 70 | MatSnackBarModule, 71 | MatSlideToggleModule, 72 | MatTabsModule, 73 | MatToolbarModule, 74 | NoRecordComponent, 75 | ReadFilePipe, 76 | ReactiveFormsModule, 77 | WarningComponent 78 | ] 79 | }) 80 | export class SharedModule { } 81 | -------------------------------------------------------------------------------- /src/app/storage-keys.ts: -------------------------------------------------------------------------------- 1 | export class StorageKeys { 2 | static KEEP_SIGNED = 'agc-keep-signed'; 3 | static AUTH_TOKEN = 'agc-auth-token'; 4 | static REMEMBER_ME = 'agc-remember-me'; 5 | static USER_EMAIL = 'agc-user-email'; 6 | static USER_PASSWORD = 'agc-user-password'; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/user/components/user-profile/user-profile.component.html: -------------------------------------------------------------------------------- 1 | 84 | 85 | 86 |

Actions

87 | 88 | 92 | 93 | 96 |
97 | -------------------------------------------------------------------------------- /src/app/user/components/user-profile/user-profile.component.scss: -------------------------------------------------------------------------------- 1 | .user-profile { 2 | margin: 20px; 3 | mat-card-content { 4 | text-align: center; 5 | } 6 | } 7 | 8 | .text-center { 9 | text-align: center; 10 | } 11 | 12 | .photo { 13 | width: 180px; 14 | height: 180px; 15 | margin: auto; 16 | position: relative; 17 | .btn-edit-user, .btn-choose-photo { 18 | position: absolute; 19 | bottom: 0; 20 | background-color: white; 21 | } 22 | .btn-edit-user { 23 | left: 0; 24 | } 25 | .btn-choose-photo { 26 | right: 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/user/components/user-profile/user-profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserProfileComponent } from './user-profile.component'; 4 | 5 | describe('UserProfileComponent', () => { 6 | let component: UserProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ UserProfileComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserProfileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/user/components/user-profile/user-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit } from '@angular/core'; 2 | import { MatDialog, MatSnackBar } from '@angular/material'; 3 | import { take } from 'rxjs/operators'; 4 | 5 | import { AuthService } from '../../../core/services/auth.service'; 6 | import { ErrorService } from '../../../core/services/error.service'; 7 | import { ImagePreviewComponent } from '../../../shared/components/image-preview/image-preview.component'; 8 | import { User } from '../../../core/models/user.model'; 9 | import { UserService } from '../../../core/services/user.service'; 10 | 11 | @Component({ 12 | selector: 'app-user-profile', 13 | templateUrl: './user-profile.component.html', 14 | styleUrls: ['./user-profile.component.scss'] 15 | }) 16 | export class UserProfileComponent implements OnInit { 17 | 18 | user: User; 19 | isEditing = false; 20 | isLoading = false; 21 | @HostBinding('class.app-user-profile') private applyHostClass = true; 22 | 23 | constructor( 24 | private authService: AuthService, 25 | private errorService: ErrorService, 26 | private dialog: MatDialog, 27 | private snackBar: MatSnackBar, 28 | private userService: UserService 29 | ) { } 30 | 31 | ngOnInit(): void { 32 | this.user = this.authService.authUser; 33 | } 34 | 35 | triggerInputFile(input: HTMLInputElement): void { 36 | input.click(); 37 | } 38 | 39 | onSelectImage(event: Event): void { 40 | const input: HTMLInputElement = event.target; 41 | const file: File = input.files[0]; 42 | const dialogRef = this.dialog.open( 43 | ImagePreviewComponent, 44 | { 45 | data: { image: file }, 46 | panelClass: 'mat-dialog-no-padding', 47 | maxHeight: '80vh' 48 | } 49 | ); 50 | 51 | dialogRef.afterClosed() 52 | .pipe(take(1)) 53 | .subscribe(dialogData => { 54 | input.value = ''; 55 | if (dialogData && dialogData.canSave) { 56 | this.isLoading = true; 57 | let message: string; 58 | this.userService.updateUserPhoto( 59 | dialogData.selectedImage, 60 | this.authService.authUser 61 | ).pipe(take(1)).subscribe( 62 | (user: User) => { 63 | message = 'Profile updated!'; 64 | this.authService.authUser.photo = user.photo; 65 | }, 66 | error => message = this.errorService.getErrorMessage(error), 67 | () => { 68 | this.isLoading = false; 69 | this.showMessage(message); 70 | } 71 | ); 72 | } 73 | }); 74 | } 75 | 76 | onSave(): void { 77 | this.isLoading = true; 78 | this.isEditing = false; 79 | let message: string; 80 | this.userService.updateUser(this.user) 81 | .pipe(take(1)) 82 | .subscribe( 83 | (user: User) => message = 'Profile updated!', 84 | error => message = this.errorService.getErrorMessage(error), 85 | () => { 86 | this.isLoading = false; 87 | this.showMessage(message); 88 | } 89 | ); 90 | } 91 | 92 | private showMessage(message: string): void { 93 | this.snackBar.open(message, 'OK', { duration: 3000, verticalPosition: 'top' }); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/app/user/user-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '../login/auth.guard'; 5 | import { UserProfileComponent } from './components/user-profile/user-profile.component'; 6 | 7 | const routes: Routes = [ 8 | { path: '', component: UserProfileComponent, canActivate: [ AuthGuard ] } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class UserRoutingModule { } 16 | -------------------------------------------------------------------------------- /src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '../shared/shared.module'; 4 | import { UserRoutingModule } from './user-routing.module'; 5 | import { UserProfileComponent } from './components/user-profile/user-profile.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | UserRoutingModule 11 | ], 12 | declarations: [UserProfileComponent] 13 | }) 14 | export class UserModule { } 15 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/group-no-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/assets/images/group-no-photo.png -------------------------------------------------------------------------------- /src/assets/images/user-no-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/assets/images/user-no-photo.png -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular Graphcool Chat 8 | 9 | 10 | 11 | 12 | 13 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /src/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-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /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 | import 'hammerjs'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.log(err)); 15 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | import * as smoothscroll from 'smoothscroll-polyfill'; 82 | smoothscroll.polyfill(); 83 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | $app-primary: mat-palette($mat-indigo); 4 | $app-gray: mat-palette($mat-gray); 5 | 6 | $primary: mat-color($app-primary, 400); 7 | $gray: mat-color($app-gray, 200); 8 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | html, body { 3 | margin: 0; 4 | } 5 | 6 | .app-login-spinner .mat-spinner circle { 7 | stroke: #fff; 8 | } 9 | 10 | .spacer { 11 | flex: 1 1 auto; 12 | } 13 | 14 | .spinner { 15 | margin: 30px auto 0 auto; 16 | } 17 | 18 | .fixed-bottom { 19 | position: fixed; 20 | bottom: 0; 21 | } 22 | 23 | .app-user-profile .mat-spinner circle { 24 | stroke: #fff; 25 | } 26 | 27 | .mat-dialog-no-padding .mat-dialog-container { 28 | padding: 0; 29 | } 30 | -------------------------------------------------------------------------------- /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: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/capa-oficial-curso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plinionaves/angular-graphcool-chat/60bb4dea295116dc06a1cd274adb9b73556a522b/static/capa-oficial-curso.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom", 18 | "esnext.asynciterable" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------