├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.ts │ │ ├── guards │ │ │ ├── auth.guard.ts │ │ │ └── non-auth.guard.ts │ │ ├── interceptors │ │ │ ├── http.error.interceptor.ts │ │ │ └── http.token.interceptor.ts │ │ └── services │ │ │ ├── api.service.ts │ │ │ ├── auth.service.ts │ │ │ ├── breakpoint.service.ts │ │ │ ├── snackbar.service.ts │ │ │ └── token.service.ts │ ├── design │ │ ├── design.module.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ └── header.component.ts │ │ └── layout │ │ │ ├── layout.component.html │ │ │ ├── layout.component.scss │ │ │ └── layout.component.ts │ ├── feature │ │ ├── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ └── dashboard.module.ts │ │ ├── landing │ │ │ ├── landing-routing.module.ts │ │ │ ├── landing.component.html │ │ │ ├── landing.component.scss │ │ │ ├── landing.component.ts │ │ │ └── landing.module.ts │ │ └── login │ │ │ ├── login-routing.module.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.ts │ │ │ └── login.module.ts │ └── shared │ │ ├── interfaces │ │ └── index.ts │ │ ├── modules │ │ └── material.module.ts │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ └── img │ │ └── POC.PNG ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts └── theme.scss ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major version 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-11 # For IE 9-11 support, remove 'not'. 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Chat 2 | ## Better Chat Experience For Streamers 3 | 4 | Better Chat is open source chat client for streamers for better chat experience. It detects the duplicated messages and adds mark on it. 5 | 6 | ## Development server 7 | 8 | Run `npm run start:dev` for a dev server. Navigate to `http://localhost:4200/`. 9 | 10 | ## Proof of concept 11 | 12 | 13 | 14 | ## Further help 15 | 16 | To get more help on contact via Discord: fishuke#2715 17 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "better-chat": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/stalk.live", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss", 32 | "src/theme.scss" 33 | ] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "namedChunks": false, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "6kb", 59 | "maximumError": "10kb" 60 | } 61 | ] 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "better-chat:build" 69 | }, 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "better-chat:build:production" 73 | } 74 | } 75 | }, 76 | "extract-i18n": { 77 | "builder": "@angular-devkit/build-angular:extract-i18n", 78 | "options": { 79 | "browserTarget": "better-chat:build" 80 | } 81 | }, 82 | "test": { 83 | "builder": "@angular-devkit/build-angular:karma", 84 | "options": { 85 | "main": "src/test.ts", 86 | "polyfills": "src/polyfills.ts", 87 | "tsConfig": "tsconfig.spec.json", 88 | "karmaConfig": "karma.conf.js", 89 | "assets": [ 90 | "src/favicon.ico", 91 | "src/assets" 92 | ], 93 | "styles": [ 94 | "src/styles.scss", 95 | "src/theme.scss" 96 | ] 97 | } 98 | }, 99 | "lint": { 100 | "builder": "@angular-devkit/build-angular:tslint", 101 | "options": { 102 | "tsConfig": [ 103 | "tsconfig.app.json", 104 | "tsconfig.spec.json", 105 | "e2e/tsconfig.json" 106 | ], 107 | "exclude": [ 108 | "**/node_modules/**" 109 | ] 110 | } 111 | }, 112 | "e2e": { 113 | "builder": "@angular-devkit/build-angular:protractor", 114 | "options": { 115 | "protractorConfig": "e2e/protractor.conf.js", 116 | "devServerTarget": "better-chat:serve" 117 | }, 118 | "configurations": { 119 | "production": { 120 | "devServerTarget": "better-chat:serve:production" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "defaultProject": "better-chat", 128 | "cli": { 129 | "analytics": false 130 | }, 131 | "schematics": { 132 | "@schematics/angular:component": { 133 | "skipTests": true 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('better-chat app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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/better-chat'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 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 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-chat", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:dev": "ng serve --port 4200", 8 | "build": "ng build --prod", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^11.0.5", 16 | "@angular/cdk": "^11.0.3", 17 | "@angular/common": "~11.0.5", 18 | "@angular/compiler": "~11.0.5", 19 | "@angular/core": "~11.0.5", 20 | "@angular/flex-layout": "^11.0.0-beta.33", 21 | "@angular/forms": "~11.0.5", 22 | "@angular/material": "^11.0.3", 23 | "@angular/platform-browser": "~11.0.5", 24 | "@angular/platform-browser-dynamic": "~11.0.5", 25 | "@angular/router": "~11.0.5", 26 | "@mdi/font": "^5.8.55", 27 | "lodash": "^4.17.20", 28 | "moment": "^2.29.1", 29 | "ngx-moment": "^5.0.0", 30 | "rxjs": "~6.6.3", 31 | "tmi.js": "^1.7.1", 32 | "tslib": "^2.0.3", 33 | "zone.js": "~0.11.3" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.1100.5", 37 | "@angular/cli": "~11.0.5", 38 | "@angular/compiler-cli": "~11.0.5", 39 | "@types/jasmine": "~3.6.2", 40 | "@types/jasminewd2": "~2.0.8", 41 | "codelyzer": "^6.0.1", 42 | "jasmine-core": "~3.6.0", 43 | "jasmine-spec-reporter": "~6.0.0", 44 | "karma": "~5.2.3", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage-istanbul-reporter": "~3.0.3", 47 | "karma-jasmine": "~4.0.1", 48 | "karma-jasmine-html-reporter": "^1.5.4", 49 | "protractor": "~7.0.0", 50 | "ts-node": "~9.1.1", 51 | "tslint": "~6.1.0", 52 | "typescript": "~4.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {LayoutComponent} from '@design/layout/layout.component'; 4 | import {NonAuthGuard} from '@core/guards/non-auth.guard'; 5 | import {AuthGuard} from '@core/guards/auth.guard'; 6 | 7 | 8 | const routes: Routes = [ 9 | { 10 | path: 'dashboard', 11 | component: LayoutComponent, 12 | children: [ 13 | { 14 | path: '', 15 | loadChildren: () => import('./feature/dashboard/dashboard.module').then(m => m.DashboardModule), 16 | canActivate: [AuthGuard], 17 | }, 18 | ] 19 | }, 20 | { 21 | path: 'landing', 22 | loadChildren: () => import('./feature/landing/landing.module').then(m => m.LandingModule) 23 | }, 24 | { 25 | path: 'login', 26 | loadChildren: () => import('./feature/login/login.module').then(m => m.LoginModule), 27 | canActivate: [NonAuthGuard], 28 | }, 29 | { 30 | path: '**', 31 | redirectTo: 'landing' 32 | } 33 | ]; 34 | 35 | @NgModule({ 36 | imports: [RouterModule.forRoot(routes)], 37 | exports: [RouterModule] 38 | }) 39 | export class AppRoutingModule { 40 | } 41 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishuke/better-chat/d844d477dfcba98e14f05d8ce4e88d83597c31aa/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'better-chat'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {AppRoutingModule} from './app-routing.module'; 3 | import {AppComponent} from './app.component'; 4 | import {CoreModule} from '@core/core.module'; 5 | import { DesignModule } from '@design/design.module'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AppComponent, 10 | ], 11 | imports: [ 12 | AppRoutingModule, 13 | CoreModule, 14 | DesignModule 15 | ], 16 | bootstrap: [AppComponent], 17 | }) 18 | export class AppModule { 19 | } 20 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule, Optional, SkipSelf} from '@angular/core'; 2 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 3 | import {BrowserModule} from '@angular/platform-browser'; 4 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 5 | import {HttpTokenInterceptor} from '@core/interceptors/http.token.interceptor'; 6 | import {ApiService} from '@core/services/api.service'; 7 | import {AuthService} from '@core/services/auth.service'; 8 | import {BreakpointService} from '@core/services/breakpoint.service'; 9 | import {TokenService} from '@core/services/token.service'; 10 | import {SnackbarService} from '@core/services/snackbar.service'; 11 | import {HttpErrorInterceptor} from '@core/interceptors/http.error.interceptor'; 12 | 13 | @NgModule({ 14 | declarations: [], 15 | imports: [ 16 | HttpClientModule, 17 | BrowserModule, 18 | BrowserAnimationsModule, 19 | ], 20 | exports: [], 21 | providers: [ 22 | { 23 | provide: HTTP_INTERCEPTORS, 24 | useClass: HttpTokenInterceptor, 25 | multi: true 26 | }, 27 | { 28 | provide: HTTP_INTERCEPTORS, 29 | useClass: HttpErrorInterceptor, 30 | multi: true 31 | }, 32 | ApiService, 33 | AuthService, 34 | BreakpointService, 35 | TokenService, 36 | SnackbarService, 37 | ] 38 | }) 39 | 40 | export class CoreModule { 41 | 42 | constructor(@Optional() @SkipSelf() coreModule: CoreModule) { 43 | 44 | if (coreModule) { 45 | throw new Error('You should import core module only in the root module'); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/app/core/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router'; 2 | import {Inject, Injectable} from '@angular/core'; 3 | import {AuthService} from '@core/services/auth.service'; 4 | import {TokenService} from '@core/services/token.service'; 5 | import {environment} from '../../../environments/environment'; 6 | import {DOCUMENT} from '@angular/common'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthGuard implements CanActivate { 12 | 13 | constructor( 14 | @Inject(DOCUMENT) private document: Document, 15 | private router: Router, 16 | private authService: AuthService, 17 | private tokenService: TokenService 18 | ) { 19 | } 20 | 21 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 22 | 23 | if (this.tokenService.getToken()) { 24 | return true; 25 | } 26 | this.document.location.href = this.authService.oauth2URL; 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/core/guards/non-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router'; 3 | import {AuthService} from '../services/auth.service'; 4 | import {TokenService} from '../services/token.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class NonAuthGuard implements CanActivate { 10 | 11 | constructor( 12 | private router: Router, 13 | private authService: AuthService, 14 | private tokenService: TokenService 15 | ) { 16 | } 17 | 18 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 19 | 20 | if (this.tokenService.getToken()) { 21 | this.router.navigate(['/']); 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/interceptors/http.error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpErrorResponse, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 3 | import {AuthService} from '../services/auth.service'; 4 | import {catchError} from 'rxjs/operators'; 5 | import {throwError} from 'rxjs'; 6 | import {Router} from '@angular/router'; 7 | 8 | @Injectable() 9 | export class HttpErrorInterceptor implements HttpInterceptor { 10 | 11 | constructor( 12 | private authService: AuthService, 13 | private router: Router 14 | ) { 15 | } 16 | 17 | intercept(request: HttpRequest, next: HttpHandler): any { 18 | 19 | return next.handle(request).pipe( 20 | catchError((error: HttpErrorResponse) => { 21 | if (error.status === 403) { 22 | this.authService.purge(); 23 | this.router.navigate(['/guilds']); 24 | } 25 | if (error.status === 404) { 26 | this.router.navigate(['/404']); 27 | } 28 | 29 | return throwError(error); 30 | }) 31 | ); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/core/interceptors/http.token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 3 | import {AuthService} from '../services/auth.service'; 4 | import {Observable} from 'rxjs'; 5 | import {TokenService} from '@core/services/token.service'; 6 | 7 | @Injectable() 8 | export class HttpTokenInterceptor implements HttpInterceptor { 9 | 10 | constructor( 11 | private authService: AuthService, 12 | private tokenService: TokenService 13 | ) { 14 | } 15 | 16 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 17 | 18 | const token = this.tokenService.getToken(); 19 | 20 | // const isLoggedIn = token && this.authService.isLoggedIn; 21 | // const isApiUrl = request.url.startsWith(environment.apiSettings.hostname); 22 | 23 | if (token) { 24 | request = request.clone({ 25 | setHeaders: { 26 | Authorization: `OAuth ${token}`, 27 | Accept: 'application/vnd.twitchtv.v5+json', 28 | 'Client-ID': 'iorij84zsvsyowclla1vnco2mqaa49' 29 | } 30 | }); 31 | } 32 | 33 | return next.handle(request); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/core/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpErrorResponse, HttpParams} from '@angular/common/http'; 3 | import {Observable, throwError} from 'rxjs'; 4 | import {catchError} from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class ApiService { 8 | 9 | private readonly baseURL: string = ''; 10 | 11 | constructor( 12 | private http: HttpClient, 13 | ) { 14 | this.baseURL = `https://api.twitch.tv/`; 15 | } 16 | 17 | private static formatErrors(errorObj: HttpErrorResponse): any { 18 | const message = JSON.stringify(errorObj.error); 19 | return throwError(message); 20 | } 21 | 22 | get(path: string, params: HttpParams = new HttpParams()): Observable { 23 | return this.http.get(`${this.baseURL}${path}`, {params}) 24 | .pipe(catchError(ApiService.formatErrors)); 25 | } 26 | 27 | post(path: string, body: object = {}): Observable { 28 | return this.http.post(`${this.baseURL}${path}`, body) 29 | .pipe(catchError(ApiService.formatErrors)); 30 | } 31 | 32 | put(path: string, body: object = {}): Observable { 33 | return this.http.put(`${this.baseURL}${path}`, body) 34 | .pipe(catchError(ApiService.formatErrors)); 35 | } 36 | 37 | patch(path: string, body: object = {}): Observable { 38 | return this.http.patch(`${this.baseURL}${path}`, body) 39 | .pipe(catchError(ApiService.formatErrors)); 40 | } 41 | 42 | delete(path: string): Observable { 43 | return this.http.delete(`${this.baseURL}${path}`) 44 | .pipe(catchError(ApiService.formatErrors)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {BehaviorSubject, Observable} from 'rxjs'; 3 | import {distinctUntilChanged} from 'rxjs/operators'; 4 | import {ApiService} from '@core/services/api.service'; 5 | import {TokenService} from '@core/services/token.service'; 6 | import {Router} from '@angular/router'; 7 | import {User} from '@shared/interfaces'; 8 | import {SnackbarService} from '@core/services/snackbar.service'; 9 | import {environment} from '../../../environments/environment'; 10 | 11 | @Injectable() 12 | export class AuthService { 13 | private userStateSubject: BehaviorSubject; 14 | public userState$: Observable; 15 | 16 | private isLoggedInSubject: BehaviorSubject; 17 | public isLoggedIn$: Observable; 18 | 19 | public oauth2URL = `https://id.twitch.tv/oauth2/authorize?client_id=${environment.CLIENT_ID}&redirect_uri=${environment.ORIGIN}/login&response_type=token&scope=user_read+chat:read`; 20 | 21 | constructor( 22 | private apiService: ApiService, 23 | private tokenService: TokenService, 24 | private router: Router, 25 | private snack: SnackbarService, 26 | private api: ApiService 27 | ) { 28 | this.userStateSubject = new BehaviorSubject(null); 29 | this.userState$ = this.userStateSubject.asObservable().pipe(distinctUntilChanged()); 30 | 31 | this.isLoggedInSubject = new BehaviorSubject(false); 32 | this.isLoggedIn$ = this.isLoggedInSubject.asObservable().pipe(distinctUntilChanged()); 33 | 34 | } 35 | 36 | 37 | public get userState(): User { 38 | return this.userStateSubject.value; 39 | } 40 | 41 | public get isLoggedIn(): boolean { 42 | return this.isLoggedInSubject.value; 43 | } 44 | 45 | async loginPromise(): Promise { 46 | await new Promise((resolve, reject) => { 47 | this.isLoggedIn$.subscribe(isLoggedIn => { 48 | if (isLoggedIn === true) { 49 | resolve(); 50 | } 51 | }); 52 | }); 53 | } 54 | 55 | 56 | check(): void { 57 | const token = this.tokenService.getToken(); 58 | 59 | if (token) { 60 | this.apiService.get('helix/user') 61 | .toPromise() 62 | .then(response => { 63 | this.set(response); 64 | console.log(response); 65 | }) 66 | .catch(() => this.purge()); 67 | } else { 68 | this.purge(); 69 | } 70 | } 71 | 72 | 73 | set(user): void { 74 | this.userStateSubject.next(user); 75 | this.isLoggedInSubject.next(true); 76 | } 77 | 78 | purge(): void { 79 | this.tokenService.destroyToken(); 80 | this.userStateSubject.next(null); 81 | this.isLoggedInSubject.next(false); 82 | // this.router.navigate(['/']).then(() => this.snack.default('Successfully logged out.')); 83 | } 84 | 85 | logout(): void { 86 | this.purge(); 87 | this.router.navigate(['/']).then(() => this.snack.default('Logged out successfully.')); 88 | } 89 | 90 | async login(token: string): Promise { 91 | this.tokenService.saveToken(token); 92 | this.check(); 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/app/core/services/breakpoint.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {BreakpointObserver, BreakpointState} from '@angular/cdk/layout'; 3 | 4 | @Injectable() 5 | export class BreakpointService { 6 | 7 | isMobile: boolean; 8 | 9 | constructor(private breakpointObserver: BreakpointObserver) { 10 | 11 | breakpointObserver 12 | .observe('(max-width: 600px)') 13 | .subscribe((data: BreakpointState) => { 14 | this.isMobile = data.matches; 15 | }); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/core/services/snackbar.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, NgZone} from '@angular/core'; 2 | import {MatSnackBar, MatSnackBarConfig} from '@angular/material/snack-bar'; 3 | 4 | @Injectable() 5 | export class SnackbarService { 6 | 7 | constructor( 8 | private readonly snackBar: MatSnackBar, 9 | private readonly zone: NgZone 10 | ) { 11 | } 12 | 13 | default(message: string): void { 14 | this.show(message, { 15 | duration: 2000, 16 | panelClass: 'default-notification-overlay' 17 | }); 18 | } 19 | 20 | info(message: string): void { 21 | this.show(message, { 22 | duration: 2000, 23 | panelClass: 'info-notification-overlay' 24 | }); 25 | } 26 | 27 | success(message: string): void { 28 | this.show(message, { 29 | duration: 2000, 30 | panelClass: 'success-notification-overlay' 31 | }); 32 | } 33 | 34 | warn(message: string): void { 35 | this.show(message, { 36 | duration: 2500, 37 | panelClass: 'warning-notification-overlay' 38 | }); 39 | } 40 | 41 | error(message: string): void { 42 | this.show(message, { 43 | duration: 3000, 44 | panelClass: 'error-notification-overlay' 45 | }); 46 | } 47 | 48 | private show(message: string, configuration: MatSnackBarConfig): void { 49 | // Need to open snackBar from Angular zone to prevent issues with its position per 50 | // https://stackoverflow.com/questions/50101912/snackbar-position-wrong-when-use-errorhandler-in-angular-5-and-material 51 | this.zone.run(() => this.snackBar.open(message, null, configuration)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/core/services/token.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable() 4 | export class TokenService { 5 | 6 | getToken(): string { 7 | return localStorage.token; 8 | } 9 | 10 | saveToken(token: string): void { 11 | localStorage.token = token; 12 | } 13 | 14 | destroyToken(): void { 15 | localStorage.removeItem('token'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/design/design.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {LayoutComponent} from './layout/layout.component'; 3 | import {HeaderComponent} from './header/header.component'; 4 | import {SharedModule} from '@shared/shared.module'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | LayoutComponent, 9 | 10 | HeaderComponent, 11 | ], 12 | imports: [ 13 | SharedModule, 14 | ], 15 | exports: [], 16 | providers: [] 17 | }) 18 | export class DesignModule { 19 | } 20 | -------------------------------------------------------------------------------- /src/app/design/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | Better Chat 3 | 4 | 5 | 6 | 7 | 8 | login 9 | Login 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{auth.userState.display_name}} 17 | arrow_drop_down 18 | 19 | 20 | 21 | 22 | exit_to_app 23 | Log Out 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/design/header/header.component.scss: -------------------------------------------------------------------------------- 1 | 2 | :host { 3 | * { 4 | outline: none; 5 | } 6 | 7 | 8 | 9 | .example-spacer { 10 | flex: 1 1 auto; 11 | } 12 | 13 | .app-logo { 14 | cursor: pointer; 15 | } 16 | 17 | button{ 18 | min-width: 0; 19 | padding-right: 0.5rem !important; 20 | padding-left: 0.5rem !important; 21 | 22 | } 23 | 24 | .mat-toolbar-row, .mat-toolbar-single-row { 25 | height: 56px; 26 | } 27 | } 28 | @media screen and (max-width: 600px) { 29 | 30 | .menu-item { 31 | display: none; 32 | clear: both; 33 | float: left; 34 | margin: 10px auto 5px 20px; 35 | width: 28%; 36 | } 37 | 38 | .mobile-hidden{ 39 | display: none; 40 | } 41 | } 42 | 43 | .profile-btn { 44 | border: 0; 45 | padding-left: 0; 46 | 47 | img { 48 | border-radius: 50%; 49 | color: white; 50 | height: 28px; 51 | width: 28px; 52 | margin-right: 0.5rem; 53 | } 54 | } 55 | 56 | .menu-item{ 57 | margin-left: 8px; 58 | letter-spacing: 1px; 59 | font-weight: normal; 60 | text-transform: uppercase; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/app/design/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {AuthService} from '@core/services/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | templateUrl: './header.component.html', 7 | styleUrls: ['./header.component.scss'] 8 | }) 9 | export class HeaderComponent { 10 | 11 | constructor(public auth: AuthService) { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/app/design/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/design/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | overflow: hidden; 6 | 7 | height: 100%; 8 | min-height: 100%; 9 | min-width: 100%; 10 | width: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/design/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AuthService} from '@core/services/auth.service'; 3 | import {BreakpointService} from '@core/services/breakpoint.service'; 4 | 5 | @Component({ 6 | selector: 'app-layout', 7 | templateUrl: './layout.component.html', 8 | styleUrls: ['./layout.component.scss'] 9 | }) 10 | export class LayoutComponent implements OnInit { 11 | 12 | constructor(public authService: AuthService, 13 | public breakpointService: BreakpointService) { 14 | 15 | } 16 | 17 | ngOnInit(): void { 18 | this.authService.check(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/feature/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { DashboardComponent } from './dashboard.component'; 5 | 6 | const routes: Routes = [{ path: '', component: DashboardComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule] 11 | }) 12 | export class DashboardRoutingModule { } 13 | -------------------------------------------------------------------------------- /src/app/feature/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Here is your chat! 8 | Go to your chat 9 | 10 | 11 | 12 | 13 | 14 | {{message.author}}: 15 | {{message.content}} 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/app/feature/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content { 2 | right: -20px; 3 | } -------------------------------------------------------------------------------- /src/app/feature/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AuthService} from '../../core/services/auth.service'; 3 | import * as tmi from 'tmi.js'; 4 | import {TokenService} from '../../core/services/token.service'; 5 | import {Message} from '../../shared/interfaces'; 6 | import * as moment from 'moment'; 7 | import * as _ from 'lodash'; 8 | 9 | @Component({ 10 | selector: 'app-dashboard', 11 | templateUrl: './dashboard.component.html', 12 | styleUrls: ['./dashboard.component.scss'] 13 | }) 14 | export class DashboardComponent implements OnInit { 15 | messageArray: Message[] = []; 16 | 17 | constructor(public auth: AuthService, private token: TokenService) { 18 | } 19 | 20 | ngOnInit(): void { 21 | // noinspection JSIgnoredPromiseFromCall 22 | this.bootstrap(); 23 | } 24 | 25 | compareMessage(first: string, second: string): boolean { 26 | first = first.replace(/\s+/g, ''); 27 | second = second.replace(/\s+/g, ''); 28 | 29 | if (!first.length && !second.length) { return true; } 30 | if (!first.length || !second.length) { return false; } 31 | if (first === second) { return true; } 32 | if (first.length === 1 && second.length === 1) { return false; } 33 | if (first.length < 2 || second.length < 2) { return false; } 34 | 35 | const letters = new Map(); 36 | let intersectionSize = 0; 37 | 38 | for (let i = 0; i < first.length - 1; i++) { 39 | const letter = first.substring(i, i + 2); 40 | const count = letters.has(letter) 41 | ? letters.get(letter) + 1 42 | : 1; 43 | 44 | letters.set(letter, count); 45 | } 46 | 47 | for (let i = 0; i < second.length - 1; i++) { 48 | const letter = second.substring(i, i + 2); 49 | const count = letters.has(letter) 50 | ? letters.get(letter) 51 | : 0; 52 | 53 | if (count > 0) { 54 | letters.set(letter, count - 1); 55 | intersectionSize++; 56 | } 57 | } 58 | 59 | const percent = (2.0 * intersectionSize) / (first.length + second.length - 2); 60 | return percent >= .5; 61 | } 62 | 63 | async bootstrap(): Promise { 64 | await this.auth.loginPromise(); 65 | const client = tmi.Client({ 66 | connection: { 67 | secure: true, 68 | reconnect: true 69 | }, 70 | identity: { 71 | username: this.auth.userState.name, 72 | password: `oauth:${this.token.getToken()}` 73 | }, 74 | channels: [this.auth.userState.name] 75 | }); 76 | client.connect().catch(console.error); 77 | 78 | // Adds 79 | client.on('message', (channel, tags, message) => { 80 | 81 | let isSentBefore = false; 82 | const idx = _.findIndex(this.messageArray, ({content}) => { 83 | return this.compareMessage(content, message); 84 | }); 85 | if (idx > -1) { isSentBefore = true; } 86 | 87 | if (isSentBefore) { 88 | const lastMessage = this.messageArray[idx]; 89 | 90 | this.messageArray.push({ 91 | content: message, 92 | author: tags['display-name'], 93 | color: tags.color, 94 | createdAt: moment().unix(), 95 | repeatCount: lastMessage.repeatCount + 1 96 | }); 97 | 98 | this.messageArray.splice(idx, 1); 99 | } else { 100 | this.messageArray.push({ 101 | content: message, 102 | author: tags['display-name'], 103 | color: tags.color, 104 | createdAt: moment().unix(), 105 | repeatCount: 1 106 | }); 107 | } 108 | }); 109 | } 110 | 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/app/feature/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { DashboardRoutingModule } from './dashboard-routing.module'; 5 | import { DashboardComponent } from './dashboard.component'; 6 | import {SharedModule} from '../../shared/shared.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [DashboardComponent], 11 | imports: [ 12 | CommonModule, 13 | DashboardRoutingModule, 14 | SharedModule 15 | ] 16 | }) 17 | export class DashboardModule { } 18 | -------------------------------------------------------------------------------- /src/app/feature/landing/landing-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LandingComponent } from './landing.component'; 5 | 6 | const routes: Routes = [{ path: '', component: LandingComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule] 11 | }) 12 | export class LandingRoutingModule { } 13 | -------------------------------------------------------------------------------- /src/app/feature/landing/landing.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Better Chat 5 | 7 | Dashboard 8 | 9 | 10 | 11 | 12 | 13 | Better Chat Experience For Streamers 14 | Better Chat is open source chat client for streamers for better chat experience. It detect 15 | the duplicated messages and adds mark on it. 16 | 17 | 18 | 21 | 23 | 24 | 25 | Contribute 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Reasons Why People Love Better Chat 37 | 38 | 39 | 40 | 41 | 42 | 43 | check 44 | 45 | Human Readable 46 | 47 | 48 | 49 | 50 | 51 | check 52 | 53 | 54 | Highly Intuitive 55 | 56 | 57 | 58 | 59 | 60 | speed 61 | 62 | 63 | Light as a Feather 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | quickreply 72 | 73 | 74 | Blazing Fast 75 | 76 | 77 | 78 | 79 | 80 | code 81 | 82 | 83 | Open Source 84 | 85 | 86 | 87 | 88 | 89 | extension 90 | 91 | 92 | Customizable 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 109 | 110 | -------------------------------------------------------------------------------- /src/app/feature/landing/landing.component.scss: -------------------------------------------------------------------------------- 1 | .bg-gradient { 2 | background: #6441a5; 3 | } 4 | 5 | .heading-brand { 6 | font-weight: 800; 7 | font-size: 1.75rem; 8 | color: white; 9 | } 10 | 11 | .svg-icon { 12 | display: -ms-inline-flexbox; 13 | display: inline-flex; 14 | -ms-flex-direction: row; 15 | flex-direction: row; 16 | -ms-flex-align: center; 17 | align-items: center; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/feature/landing/landing.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {AuthService} from '../../core/services/auth.service'; 3 | 4 | @Component({ 5 | selector: 'app-landing', 6 | templateUrl: './landing.component.html', 7 | styleUrls: ['./landing.component.scss'] 8 | }) 9 | export class LandingComponent implements OnInit { 10 | 11 | constructor(public auth: AuthService) { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/feature/landing/landing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { LandingRoutingModule } from './landing-routing.module'; 5 | import { LandingComponent } from './landing.component'; 6 | import {SharedModule} from '../../shared/shared.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [LandingComponent], 11 | imports: [ 12 | CommonModule, 13 | LandingRoutingModule, 14 | SharedModule 15 | ] 16 | }) 17 | export class LandingModule { } 18 | -------------------------------------------------------------------------------- /src/app/feature/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import {LoginComponent} from './login.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: LoginComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class LoginRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/feature/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/feature/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishuke/better-chat/d844d477dfcba98e14f05d8ce4e88d83597c31aa/src/app/feature/login/login.component.scss -------------------------------------------------------------------------------- /src/app/feature/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {DOCUMENT} from '@angular/common'; 4 | import {AuthService} from '../../core/services/auth.service'; 5 | import {environment} from '../../../environments/environment'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-login', 10 | templateUrl: './login.component.html', 11 | styleUrls: ['./login.component.scss'] 12 | }) 13 | export class LoginComponent implements OnInit { 14 | constructor(@Inject(DOCUMENT) private document: Document, 15 | private route: ActivatedRoute, 16 | private auth: AuthService) { 17 | 18 | } 19 | 20 | ngOnInit(): void { 21 | this.route.fragment.subscribe(fragment => { 22 | if (fragment) { 23 | const params = fragment.split('&'); 24 | if (params[0] && params[1] && params[2]) { 25 | const token = params[0].slice(13); 26 | const scopes = params[1].slice(6); 27 | const tokenType = params[2].slice(11); 28 | console.log({ 29 | token, scopes, tokenType 30 | }); 31 | if (scopes !== 'user_read+chat:read') { 32 | this.redirectToHome(); 33 | } 34 | if (tokenType !== 'bearer') { 35 | this.redirectToHome(); 36 | } 37 | if (token){ 38 | this.auth.login(token).then(() => this.redirectToDashboard()); 39 | } 40 | } else { 41 | this.redirectToHome(); 42 | } 43 | } else { 44 | this.redirectToHome(); 45 | } 46 | }); 47 | 48 | } 49 | 50 | redirectToHome(): void { 51 | this.document.location.href = environment.ORIGIN; 52 | } 53 | redirectToDashboard(): void { 54 | this.document.location.href = `${environment.ORIGIN}/dashboard`; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/feature/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { LoginRoutingModule } from './login-routing.module'; 5 | import {LoginComponent} from './login.component'; 6 | import {SharedModule} from '../../shared/shared.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [LoginComponent], 11 | imports: [ 12 | CommonModule, 13 | LoginRoutingModule, 14 | SharedModule 15 | ] 16 | }) 17 | export class LoginModule { } 18 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id: number; 3 | bio: string; 4 | created_at: string; 5 | display_name: string; 6 | email: string; 7 | email_verified: boolean; 8 | logo: string; 9 | name: string; 10 | notifications: { 11 | email: boolean; 12 | push: boolean; 13 | }; 14 | partnered: boolean; 15 | twitter_connected: boolean; 16 | type: string; 17 | updated_at: string; 18 | } 19 | 20 | export interface Message { 21 | content: string; 22 | author: string; 23 | color: string; 24 | createdAt: number; 25 | repeatCount: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/modules/material.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | 3 | import {MatIconModule} from '@angular/material/icon'; 4 | import {MatCardModule} from '@angular/material/card'; 5 | import {MatListModule} from '@angular/material/list'; 6 | import {MatMenuModule} from '@angular/material/menu'; 7 | import {MatInputModule} from '@angular/material/input'; 8 | import {MatChipsModule} from '@angular/material/chips'; 9 | import {MatBadgeModule} from '@angular/material/badge'; 10 | import {MatButtonModule} from '@angular/material/button'; 11 | import {MatToolbarModule} from '@angular/material/toolbar'; 12 | import {MatDividerModule} from '@angular/material/divider'; 13 | import {MatTooltipModule} from '@angular/material/tooltip'; 14 | import {MatSidenavModule} from '@angular/material/sidenav'; 15 | import {MatCheckboxModule} from '@angular/material/checkbox'; 16 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 17 | import {MatFormFieldModule} from '@angular/material/form-field'; 18 | import {MatExpansionModule} from '@angular/material/expansion'; 19 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 20 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 21 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 22 | import {MatButtonToggleModule} from '@angular/material/button-toggle'; 23 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 24 | import {MatTableModule} from '@angular/material/table'; 25 | import {MatSelectModule} from '@angular/material/select'; 26 | import {MatDialogModule} from '@angular/material/dialog'; 27 | import {MatRadioModule} from '@angular/material/radio'; 28 | import {MatTabsModule} from '@angular/material/tabs'; 29 | import {MatPaginatorModule} from '@angular/material/paginator'; 30 | import {MatRippleModule} from '@angular/material/core'; 31 | import {DragDropModule} from '@angular/cdk/drag-drop'; 32 | 33 | const MaterialModules = [ 34 | MatIconModule, 35 | MatCardModule, 36 | MatListModule, 37 | MatMenuModule, 38 | MatInputModule, 39 | MatChipsModule, 40 | MatBadgeModule, 41 | MatButtonModule, 42 | MatToolbarModule, 43 | MatDividerModule, 44 | MatTooltipModule, 45 | MatSidenavModule, 46 | MatCheckboxModule, 47 | MatSnackBarModule, 48 | MatFormFieldModule, 49 | MatExpansionModule, 50 | MatSlideToggleModule, 51 | MatProgressBarModule, 52 | MatAutocompleteModule, 53 | MatButtonToggleModule, 54 | MatProgressSpinnerModule, 55 | MatTableModule, 56 | MatRadioModule, 57 | MatSelectModule, 58 | MatDialogModule, 59 | MatTabsModule, 60 | MatPaginatorModule, 61 | MatRippleModule, 62 | DragDropModule 63 | ]; 64 | 65 | @NgModule({ 66 | imports: [ 67 | ...MaterialModules 68 | ], 69 | exports: [ 70 | ...MaterialModules 71 | ] 72 | }) 73 | export class MaterialModule { 74 | } 75 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {MaterialModule} from '@shared/modules/material.module'; 3 | import {FlexLayoutModule} from '@angular/flex-layout'; 4 | import {CommonModule} from '@angular/common'; 5 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 6 | import {RouterModule} from '@angular/router'; 7 | import {MomentModule} from 'ngx-moment'; 8 | 9 | const SharedModules = [ 10 | CommonModule, 11 | FlexLayoutModule, 12 | MaterialModule, 13 | ReactiveFormsModule, 14 | RouterModule, 15 | FormsModule, 16 | MomentModule, 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [ 21 | ...SharedModules, 22 | ], 23 | exports: [ 24 | ...SharedModules, 25 | ], 26 | }) 27 | export class SharedModule { 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishuke/better-chat/d844d477dfcba98e14f05d8ce4e88d83597c31aa/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/POC.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishuke/better-chat/d844d477dfcba98e14f05d8ce4e88d83597c31aa/src/assets/img/POC.PNG -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | ORIGIN: 'https://fishuke.github.io/better-chat', 4 | CLIENT_ID: 'iorij84zsvsyowclla1vnco2mqaa49', 5 | WS_URL: 'wss://irc-ws.chat.twitch.tv:443' 6 | }; 7 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | ORIGIN: 'http://localhost:4200', 4 | CLIENT_ID: 'iorij84zsvsyowclla1vnco2mqaa49', 5 | WS_URL: 'ws://irc-ws.chat.twitch.tv:80' 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishuke/better-chat/d844d477dfcba98e14f05d8ce4e88d83597c31aa/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Better Chat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | outline: none; 12 | } 13 | 14 | router-outlet { 15 | display: none; 16 | } 17 | 18 | .mat-expansion-indicator 19 | { 20 | margin-right: 10px; 21 | margin-bottom: 5px; 22 | } 23 | 24 | .mat-expansion-indicator:after { 25 | padding: 8px !important; 26 | margin-bottom: 5px !important; 27 | } 28 | 29 | .mat-drawer-backdrop.mat-drawer-shown { 30 | background-color: rgba(0, 0, 0, 0.6) !important; 31 | } 32 | 33 | .mat-drawer-inner-container { 34 | overflow: hidden !important; 35 | } 36 | 37 | canvas { 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | } 42 | 43 | html { 44 | scrollbar-face-color: #646464; 45 | scrollbar-base-color: #646464; 46 | scrollbar-3dlight-color: #646464; 47 | scrollbar-highlight-color: #646464; 48 | scrollbar-track-color: #000; 49 | scrollbar-arrow-color: #000; 50 | scrollbar-shadow-color: #646464; 51 | } 52 | 53 | ::-webkit-scrollbar { 54 | width: 8px; 55 | height: 3px; 56 | } 57 | 58 | ::-webkit-scrollbar-button { 59 | background-color: #666; 60 | } 61 | 62 | ::-webkit-scrollbar-track { 63 | background-color: #646464; 64 | } 65 | 66 | ::-webkit-scrollbar-track-piece { 67 | background-color: #000; 68 | } 69 | 70 | ::-webkit-scrollbar-thumb { 71 | height: 50px; 72 | background-color: #666; 73 | } 74 | 75 | ::-webkit-scrollbar-corner { 76 | background-color: #646464; 77 | } 78 | 79 | ::-webkit-resizer { 80 | background-color: #666; 81 | } 82 | @media screen and (max-width: 400px) { 83 | .cdk-overlay-pane{ 84 | max-width: 100% !important; 85 | } 86 | } 87 | @media screen and (min-width: 600px) { 88 | .cdk-overlay-pane{ 89 | max-width: 60% !important; 90 | } 91 | } 92 | @media screen and (min-width: 800px) { 93 | .cdk-overlay-pane{ 94 | max-width: 50% !important; 95 | } 96 | } 97 | @media screen and (min-width: 800px) { 98 | .cdk-overlay-pane{ 99 | max-width: 500px !important; 100 | } 101 | } 102 | 103 | .mat-menu-panel { 104 | min-height: 10px !important; 105 | color: white; 106 | } 107 | 108 | html, body { 109 | height: 100%; 110 | } 111 | 112 | body { 113 | margin: 0; 114 | -webkit-font-smoothing: antialiased; 115 | } 116 | 117 | * { 118 | box-sizing: border-box; 119 | outline: none; 120 | } 121 | 122 | router-outlet { 123 | display: none; 124 | } 125 | 126 | [disabled] { 127 | cursor: not-allowed !important; 128 | } 129 | 130 | button:focus{ 131 | outline: none !important; 132 | } 133 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/theme.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | // Override the typography in the core CSS. 4 | @include mat-core(); 5 | 6 | $primary-color: ( 7 | 50 : #e8e0f4, 8 | 100 : #c6b3e3, 9 | 200 : #a180d1, 10 | 300 : #7b4dbe, 11 | 400 : #5e26b0, 12 | 500 : #4200a2, 13 | 600 : #3c009a, 14 | 700 : #330090, 15 | 800 : #2b0086, 16 | 900 : #1d0075, 17 | A100 : #b3a3ff, 18 | A200 : #8870ff, 19 | A400 : #5e3dff, 20 | A700 : #4824ff, 21 | contrast: ( 22 | 50 : #000000, 23 | 100 : #000000, 24 | 200 : #000000, 25 | 300 : #ffffff, 26 | 400 : #ffffff, 27 | 500 : #ffffff, 28 | 600 : #ffffff, 29 | 700 : #ffffff, 30 | 800 : #ffffff, 31 | 900 : #ffffff, 32 | A100 : #000000, 33 | A200 : #000000, 34 | A400 : #ffffff, 35 | A700 : #ffffff, 36 | ) 37 | ); 38 | 39 | 40 | $accent-color: ( 41 | 50 : #e5e5e5, 42 | 100 : #bdbebf, 43 | 200 : #919395, 44 | 300 : #65686a, 45 | 400 : #44474a, 46 | 500 : #23272a, 47 | 600 : #1f2325, 48 | 700 : #1a1d1f, 49 | 800 : #151719, 50 | 900 : #0c0e0f, 51 | A100 : #55aaff, 52 | A200 : #2290ff, 53 | A400 : #0077ee, 54 | A700 : #006ad4, 55 | contrast: ( 56 | 50 : #000000, 57 | 100 : #000000, 58 | 200 : #000000, 59 | 300 : #ffffff, 60 | 400 : #ffffff, 61 | 500 : #ffffff, 62 | 600 : #ffffff, 63 | 700 : #ffffff, 64 | 800 : #ffffff, 65 | 900 : #ffffff, 66 | A100 : #000000, 67 | A200 : #ffffff, 68 | A400 : #ffffff, 69 | A700 : #ffffff, 70 | ) 71 | ); 72 | 73 | 74 | 75 | $primary: mat-palette($primary-color, 700); 76 | $accent: mat-palette($accent-color); 77 | $warn: mat-palette($mat-red); 78 | 79 | $theme: mat-dark-theme($primary, $accent, $warn); 80 | 81 | @include angular-material-theme($theme); 82 | 83 | .mat-snack-bar-container { 84 | color: #000; 85 | } 86 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "outDir": "./out-tsc/app", 7 | "types": [], 8 | "paths": { 9 | "@core/*": [ 10 | "src/app/core/*" 11 | ], 12 | "@design/*": [ 13 | "src/app/design/*" 14 | ], 15 | "@feature/*": [ 16 | "src/app/feature/*" 17 | ], 18 | "@shared/*": [ 19 | "src/app/shared/*" 20 | ], 21 | "@assets/*": [ 22 | "src/assets/*" 23 | ] 24 | } 25 | }, 26 | "files": [ 27 | "src/main.ts", 28 | "src/polyfills.ts" 29 | ], 30 | "include": [ 31 | "src/**/*.d.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.spec.json" 9 | }, 10 | { 11 | "path": "./e2e/tsconfig.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-return-shorthand": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "directive-class-suffix": true, 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "eofline": true, 32 | "import-blacklist": [ 33 | true, 34 | "rxjs/Rx" 35 | ], 36 | "import-spacing": true, 37 | "indent": { 38 | "options": [ 39 | "spaces" 40 | ] 41 | }, 42 | "max-classes-per-file": false, 43 | "max-line-length": [ 44 | true, 45 | 140 46 | ], 47 | "member-ordering": [ 48 | true, 49 | { 50 | "order": [ 51 | "static-field", 52 | "instance-field", 53 | "static-method", 54 | "instance-method" 55 | ] 56 | } 57 | ], 58 | "no-console": [ 59 | true, 60 | "debug", 61 | "info", 62 | "time", 63 | "timeEnd", 64 | "trace" 65 | ], 66 | "no-empty": false, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-non-null-assertion": true, 72 | "no-redundant-jsdoc": true, 73 | "no-switch-case-fall-through": true, 74 | "no-var-requires": false, 75 | "object-literal-key-quotes": [ 76 | true, 77 | "as-needed" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "semicolon": { 84 | "options": [ 85 | "always" 86 | ] 87 | }, 88 | "space-before-function-paren": { 89 | "options": { 90 | "anonymous": "never", 91 | "asyncArrow": "always", 92 | "constructor": "never", 93 | "method": "never", 94 | "named": "never" 95 | } 96 | }, 97 | "typedef": [ 98 | true, 99 | "call-signature" 100 | ], 101 | "typedef-whitespace": { 102 | "options": [ 103 | { 104 | "call-signature": "nospace", 105 | "index-signature": "nospace", 106 | "parameter": "nospace", 107 | "property-declaration": "nospace", 108 | "variable-declaration": "nospace" 109 | }, 110 | { 111 | "call-signature": "onespace", 112 | "index-signature": "onespace", 113 | "parameter": "onespace", 114 | "property-declaration": "onespace", 115 | "variable-declaration": "onespace" 116 | } 117 | ] 118 | }, 119 | "variable-name": { 120 | "options": [ 121 | "ban-keywords", 122 | "check-format", 123 | "allow-pascal-case" 124 | ] 125 | }, 126 | "whitespace": { 127 | "options": [ 128 | "check-branch", 129 | "check-decl", 130 | "check-operator", 131 | "check-separator", 132 | "check-type", 133 | "check-typecast" 134 | ] 135 | }, 136 | "no-conflicting-lifecycle": true, 137 | "no-host-metadata-property": true, 138 | "no-input-rename": true, 139 | "no-inputs-metadata-property": true, 140 | "no-output-native": true, 141 | "no-output-on-prefix": true, 142 | "no-output-rename": true, 143 | "no-outputs-metadata-property": true, 144 | "template-banana-in-box": true, 145 | "template-no-negated-async": true, 146 | "use-lifecycle-interface": true, 147 | "use-pipe-transform-interface": true 148 | }, 149 | "rulesDirectory": [ 150 | "codelyzer" 151 | ] 152 | } --------------------------------------------------------------------------------
Better Chat is open source chat client for streamers for better chat experience. It detect 15 | the duplicated messages and adds mark on it.