├── src ├── assets │ ├── .gitkeep │ ├── splash.jpg │ ├── discreet-irc.jpeg │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png │ ├── audio │ │ ├── notifications.mp3 │ │ ├── just-like-that.mp3 │ │ └── notification_2.mp3 │ └── server-list.json ├── app │ ├── chat │ │ ├── dialogs │ │ │ ├── action-prompt │ │ │ │ ├── action-prompt.component.scss │ │ │ │ ├── action-prompt.component.ts │ │ │ │ ├── action-prompt.component.spec.ts │ │ │ │ └── action-prompt.component.html │ │ │ ├── emoji-dialog │ │ │ │ ├── emoji-dialog.component.scss │ │ │ │ ├── emoji-dialog.component.html │ │ │ │ ├── emoji-dialog.component.ts │ │ │ │ └── emoji-dialog.component.spec.ts │ │ │ ├── away-prompt │ │ │ │ ├── away-prompt.component.scss │ │ │ │ ├── away-prompt.component.ts │ │ │ │ ├── away-prompt.component.spec.ts │ │ │ │ └── away-prompt.component.html │ │ │ ├── nickname-prompt │ │ │ │ ├── nickname-prompt.component.scss │ │ │ │ ├── nickname-prompt.component.ts │ │ │ │ ├── nickname-prompt.component.spec.ts │ │ │ │ └── nickname-prompt.component.html │ │ │ ├── media-playlist │ │ │ │ ├── media-playlist.component.scss │ │ │ │ ├── media-playlist.component.spec.ts │ │ │ │ ├── media-playlist.component.ts │ │ │ │ └── media-playlist.component.html │ │ │ ├── channels-list │ │ │ │ ├── channels-list.component.scss │ │ │ │ ├── channels-list.component.spec.ts │ │ │ │ ├── channels-list.component.ts │ │ │ │ └── channels-list.component.html │ │ │ ├── user-info-dialog │ │ │ │ ├── user-info-dialog.component.scss │ │ │ │ ├── user-info-dialog.component.spec.ts │ │ │ │ ├── user-info-dialog.component.ts │ │ │ │ └── user-info-dialog.component.html │ │ │ └── youtube-search │ │ │ │ ├── youtube-search.component.scss │ │ │ │ ├── youtube-search.component.spec.ts │ │ │ │ ├── youtube-search.component.ts │ │ │ │ └── youtube-search.component.html │ │ ├── pipes │ │ │ ├── callback.pipe.ts │ │ │ ├── sort-by.pipe.ts │ │ │ ├── safe.pipe.ts │ │ │ └── enrich-message.pipe.ts │ │ ├── chat-info.ts │ │ ├── chat-message.ts │ │ ├── chat-manager │ │ │ ├── chat-manager.component.spec.ts │ │ │ ├── chat-manager.component.scss │ │ │ └── chat-manager.component.html │ │ ├── messages-window │ │ │ ├── messages-window.component.spec.ts │ │ │ ├── messages-window.component.ts │ │ │ ├── messages-window.component.scss │ │ │ └── messages-window.component.html │ │ ├── chat-user.ts │ │ ├── private-chat.ts │ │ ├── public-chat.ts │ │ ├── text-formatting.ts │ │ └── chat-data.ts │ ├── irc-client-service │ │ ├── irc-util.ts │ │ ├── irc-channel.ts │ │ ├── login-info.ts │ │ ├── irc-server.ts │ │ └── irc-user.ts │ ├── app-routing.module.ts │ ├── services │ │ ├── encr-decr.service.spec.ts │ │ ├── settings.service.spec.ts │ │ ├── youtube-search.service.spec.ts │ │ ├── pouchdb.service.spec.ts │ │ ├── youtube-search.service.ts │ │ ├── encr-decr.service.ts │ │ ├── settings.service.ts │ │ └── pouchdb.service.ts │ ├── splash-screen │ │ ├── splash-screen.component.scss │ │ ├── splash-screen.component.spec.ts │ │ ├── splash-screen.component.html │ │ └── splash-screen.component.ts │ ├── socialmedia │ │ └── youtube-video │ │ │ ├── youtube-video.component.spec.ts │ │ │ ├── youtube-video.component.scss │ │ │ ├── youtube-video.component.html │ │ │ └── youtube-video.component.ts │ ├── app.component.spec.ts │ ├── app.component.scss │ ├── material.module.ts │ ├── app.module.ts │ ├── app.component.html │ └── app.component.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── main.ts ├── index-language-detect.html ├── test.ts ├── manifest.webmanifest ├── index.html ├── themes │ └── material-theme.scss ├── styles.scss ├── polyfills.ts └── locale │ ├── messages.en.xlf │ └── messages.it.xlf ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── dist └── index.html ├── tsconfig.json ├── ngsw-config.json ├── .gitignore ├── karma.conf.js ├── LICENSE ├── tslint.json ├── package.json ├── README.md └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/action-prompt/action-prompt.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/emoji-dialog/emoji-dialog.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/irc-client-service/irc-util.ts: -------------------------------------------------------------------------------- 1 | export class IrcUtil { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/splash.jpg -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/away-prompt/away-prompt.component.scss: -------------------------------------------------------------------------------- 1 | strong { 2 | margin-bottom: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/discreet-irc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/discreet-irc.jpeg -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/audio/notifications.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/audio/notifications.mp3 -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/audio/just-like-that.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/audio/just-like-that.mp3 -------------------------------------------------------------------------------- /src/assets/audio/notification_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/genielabs/discreet/HEAD/src/assets/audio/notification_2.mp3 -------------------------------------------------------------------------------- /src/app/irc-client-service/irc-channel.ts: -------------------------------------------------------------------------------- 1 | export class IrcChannel { 2 | name: string; 3 | users: number; 4 | modes: string; 5 | topic: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/irc-client-service/login-info.ts: -------------------------------------------------------------------------------- 1 | import {IrcServer} from './irc-server'; 2 | 3 | export class LoginInfo { 4 | server: IrcServer; 5 | nick: string; 6 | password: string; 7 | autoJoin = [] as string[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/nickname-prompt/nickname-prompt.component.scss: -------------------------------------------------------------------------------- 1 | button { 2 | margin-top: 16px; 3 | } 4 | mat-checkbox { 5 | margin-top: 8px; 6 | margin-bottom: 8px; 7 | } 8 | .mat-dialog-content { 9 | min-height: 112px; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/server-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dev", 4 | "name": "localhost", 5 | "address": "localhost:6667", 6 | "webSocketUrl": "ws://localhost:7002", 7 | "channels": [], 8 | "timestamp": 0, 9 | "hidden": false 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | 5 | const routes: Routes = []; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forRoot(routes)], 9 | exports: [RouterModule] 10 | }) 11 | export class AppRoutingModule { } 12 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/irc-client-service/irc-server.ts: -------------------------------------------------------------------------------- 1 | import {IrcChannel} from './irc-channel'; 2 | 3 | export class IrcServer { 4 | id: string; 5 | address: string; 6 | name: string; 7 | description?: string; 8 | webSocketUrl: string; 9 | channels = [] as IrcChannel[]; 10 | timestamp = 0; 11 | hidden?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 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 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/away-prompt/away-prompt.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-away-prompt', 5 | templateUrl: './away-prompt.component.html', 6 | styleUrls: ['./away-prompt.component.scss'] 7 | }) 8 | export class AwayPromptComponent { 9 | awayText = 'Ain\'t here right now, but I\'ll be right back!'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/chat/pipes/callback.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Pipe } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'callback', 5 | pure: false 6 | }) 7 | export class CallbackPipe implements PipeTransform { 8 | transform(items: any[], callback: (item: any) => boolean): any { 9 | if (!items || !callback) { 10 | return items; 11 | } 12 | return items.filter(item => callback(item)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/irc-client-service/irc-user.ts: -------------------------------------------------------------------------------- 1 | export class IrcUser { 2 | prefix: string; 3 | name: string; 4 | mode: string; 5 | away: string; 6 | whois: any = {}; 7 | version: string; 8 | online = true; 9 | channels: {mode: string, flags: string} [] = []; 10 | constructor() { 11 | // this is used to store private chat data like chatUser playlists 12 | this.channels['private'] = {}; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/services/encr-decr.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { EncrDecrService } from './encr-decr.service'; 4 | 5 | describe('EncrDecrService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: EncrDecrService = TestBed.get(EncrDecrService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/services/settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsService } from './settings.service'; 4 | 5 | describe('SettingsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SettingsService = TestBed.get(SettingsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/media-playlist/media-playlist.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-bottom: 8px; 3 | } 4 | .title { 5 | display: flex; 6 | align-items: center; 7 | font-weight: 600; 8 | margin: 0; 9 | } 10 | .title .mat-icon { 11 | margin-right: 8px; 12 | } 13 | .media-image { 14 | margin-right: 8px; 15 | } 16 | .media-title { 17 | overflow: hidden; 18 | height: 56px; 19 | margin-bottom: 16px; 20 | font-size: 87%; 21 | } 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/app/services/youtube-search.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { YoutubeSearchService } from './youtube-search.service'; 4 | 5 | describe('YoutubeSearchService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: YoutubeSearchService = TestBed.get(YoutubeSearchService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/channels-list/channels-list.component.scss: -------------------------------------------------------------------------------- 1 | .row { 2 | cursor: pointer; 3 | height: 40px; 4 | } 5 | .v-scroll { 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | } 9 | .list-height { 10 | height: 420px; 11 | } 12 | .channels-count { 13 | font-size: 80%; 14 | opacity: 0.5; 15 | } 16 | .loader { 17 | position: absolute; 18 | top: calc(50% - 60px); 19 | left: calc(50% - 60px + 12px); 20 | width: 120px; 21 | height: 120px; 22 | } 23 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 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 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /src/app/chat/chat-info.ts: -------------------------------------------------------------------------------- 1 | export class ChatInfo { 2 | name: string; 3 | host: string; 4 | modes: string; 5 | prefix: string; 6 | // TODO: add more data 7 | type: 'public' | 'private' = 'private'; 8 | 9 | constructor(prefix: string) { 10 | this.name = this.prefix = prefix; 11 | if (prefix.indexOf('!') !== -1) { 12 | [this.name, this.host] = prefix.split('!'); 13 | } 14 | if (this.name.startsWith('#')) { 15 | this.type = 'public'; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/pouchdb.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async, inject } from '@angular/core/testing'; 4 | import { PouchDBService } from './pouchdb.service'; 5 | 6 | describe('PouchDBService', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [PouchDBService] 10 | }); 11 | }); 12 | 13 | it('should ...', inject([PouchdbService], (service: PouchdbService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/action-prompt/action-prompt.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from '@angular/core'; 2 | import {MAT_DIALOG_DATA} from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-action-prompt', 6 | templateUrl: './action-prompt.component.html', 7 | styleUrls: ['./action-prompt.component.scss'] 8 | }) 9 | export class ActionPromptComponent { 10 | actionText = 'greets everyone!'; 11 | 12 | constructor( 13 | @Inject(MAT_DIALOG_DATA) public username: string 14 | ) { } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/user-info-dialog/user-info-dialog.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | max-width: 320px; 3 | word-break: break-all; 4 | } 5 | 6 | .title { 7 | display: flex; 8 | align-items: center; 9 | font-weight: 600; 10 | margin: 0; 11 | } 12 | .title .mat-icon { 13 | margin-right: 8px; 14 | } 15 | label { 16 | font-weight: bold; 17 | margin-top: 8px; 18 | display: block; 19 | } 20 | .address { 21 | opacity: 0.5; 22 | font-size: 85%; 23 | } 24 | .date { 25 | margin-top: 10px; 26 | margin-bottom: 10px; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/emoji-dialog/emoji-dialog.component.html: -------------------------------------------------------------------------------- 1 | 6 |
7 | 11 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/chat/pipes/sort-by.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Pipe({ 5 | name: 'sortBy' 6 | }) 7 | export class SortByPipe implements PipeTransform { 8 | 9 | transform(value: any[], order = '', column = ''): any[] { 10 | if (!value || order === '' || !order) { return value; } // no array 11 | if (!column || column === '') { return _.sortBy(value); } // sort 1d array 12 | if (value.length <= 1) { return value; } // array with only one item 13 | return _.orderBy(value, [column], [order]); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/splash-screen/splash-screen.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: absolute; 3 | left: 0; right: 0; 4 | top: 0; bottom: 0; 5 | } 6 | .page { 7 | height: 100%; 8 | background-image: url('../../assets/splash.jpg'); 9 | background-position: left; 10 | background-repeat: no-repeat; 11 | background-size: cover; 12 | } 13 | button { 14 | margin-top: 16px; 15 | } 16 | mat-checkbox { 17 | margin-top: 8px; 18 | margin-bottom: 8px; 19 | } 20 | mat-card { 21 | opacity: 0.9; 22 | } 23 | .content { 24 | min-width: 240px; 25 | } 26 | .hidden { 27 | display: none !important; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/emoji-dialog/emoji-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Inject, OnInit} from '@angular/core'; 2 | import {MatDialogRef} from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-emoji-dialog', 6 | templateUrl: './emoji-dialog.component.html', 7 | styleUrls: ['./emoji-dialog.component.scss'] 8 | }) 9 | export class EmojiDialogComponent implements OnInit { 10 | public emojiClicked = new EventEmitter(); 11 | 12 | constructor( 13 | public dialogRef: MatDialogRef 14 | ) { } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/chat/chat-message.ts: -------------------------------------------------------------------------------- 1 | export class ChatMessage { 2 | type?: ChatMessageType = ChatMessageType.MESSAGE; 3 | sender: string; 4 | target: string; 5 | message: string; 6 | data?: any; 7 | timestamp?: number = Date.now(); 8 | isLocal?: boolean; 9 | // data for visualization 10 | rendered = {} as { 11 | message?: string; 12 | flagsIconColor?: string; 13 | flagsIconName?: string; 14 | musicIcon?: string; 15 | date?: string; 16 | }; 17 | } 18 | 19 | export enum ChatMessageType { 20 | MESSAGE, 21 | ACTION, 22 | JOIN, 23 | PART, 24 | KICK, 25 | QUIT, 26 | MODE 27 | } 28 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/nickname-prompt/nickname-prompt.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {MAT_DIALOG_DATA} from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-nickname-prompt', 6 | templateUrl: './nickname-prompt.component.html', 7 | styleUrls: ['./nickname-prompt.component.scss'] 8 | }) 9 | export class NicknamePromptComponent implements OnInit { 10 | nick = 'Ospite-' + Math.ceil(Math.random() * 1000); 11 | password = ''; 12 | 13 | constructor( 14 | @Inject(MAT_DIALOG_DATA) public loginInfo: any 15 | ) { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Discreet IRC client built with Angular 6 | 7 | 8 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/youtube-search/youtube-search.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-bottom: 8px; 3 | } 4 | .title { 5 | position: relative; 6 | display: flex; 7 | align-items: center; 8 | font-weight: 600; 9 | margin: 0; 10 | } 11 | .title .mat-icon { 12 | margin-right: 8px; 13 | } 14 | .media-container { 15 | margin-top: 16px; 16 | margin-bottom: 16px; 17 | } 18 | .media-image { 19 | margin-right: 8px; 20 | } 21 | .media-title { 22 | overflow: hidden; 23 | height: 56px; 24 | font-size: 87%; 25 | } 26 | .media-actions { 27 | opacity: 0.5; 28 | } 29 | .close-button { 30 | position: absolute; 31 | top: -12px; 32 | right: -12px; 33 | } 34 | -------------------------------------------------------------------------------- /src/index-language-detect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Discreet IRC client built with Angular 6 | 7 | 8 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es5", 14 | "types": [ 15 | "node", 16 | "webpack-env" 17 | ], 18 | "typeRoots": [ 19 | "node_modules/@types" 20 | ], 21 | "lib": [ 22 | "es2018", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "fullTemplateTypeCheck": true, 28 | "strictInjectionParameters": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /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('ng-web-irc 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 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/app/services/youtube-search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as search from 'youtube-search'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class YoutubeSearchService { 8 | // Discreet PWA official API Key for searchinng public YT videos 9 | private apiKey = 'AIzaSyC_tAE9m1UBb-rsv-sthNWb2NyDkhWG8-c'; 10 | 11 | constructor() { } 12 | 13 | search(query: string, callback) { 14 | const opts: search.YouTubeSearchOptions = { 15 | maxResults: 20, 16 | type: 'video', 17 | key: this.apiKey 18 | }; 19 | search(query, opts, (err, results) => { 20 | if (err) { 21 | return console.log(err); 22 | } 23 | if (callback) { 24 | callback(results); 25 | } 26 | }); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/app/chat/chat-manager/chat-manager.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatManagerComponent } from './chat-manager.component'; 4 | 5 | describe('ChatManagerComponent', () => { 6 | let component: ChatManagerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChatManagerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatManagerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/away-prompt/away-prompt.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AwayPromptComponent } from './away-prompt.component'; 4 | 5 | describe('AwayPromptComponent', () => { 6 | let component: AwayPromptComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AwayPromptComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AwayPromptComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/splash-screen/splash-screen.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SplashScreenComponent } from './splash-screen.component'; 4 | 5 | describe('SplashScreenComponent', () => { 6 | let component: SplashScreenComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SplashScreenComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SplashScreenComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/emoji-dialog/emoji-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EmojiDialogComponent } from './emoji-dialog.component'; 4 | 5 | describe('EmojiDialogComponent', () => { 6 | let component: EmojiDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ EmojiDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EmojiDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/action-prompt/action-prompt.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ActionPromptComponent } from './action-prompt.component'; 4 | 5 | describe('ActionPromptComponent', () => { 6 | let component: ActionPromptComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ActionPromptComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ActionPromptComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/channels-list/channels-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChannelsListComponent } from './channels-list.component'; 4 | 5 | describe('ChannelsListComponent', () => { 6 | let component: ChannelsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ChannelsListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChannelsListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/socialmedia/youtube-video/youtube-video.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { YoutubeVideoComponent } from './youtube-video.component'; 4 | 5 | describe('YoutubeVideoComponent', () => { 6 | let component: YoutubeVideoComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ YoutubeVideoComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(YoutubeVideoComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/away-prompt/away-prompt.component.html: -------------------------------------------------------------------------------- 1 |

Set away

2 | 3 | When set away, users writing to you will receive an automatic message. 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/media-playlist/media-playlist.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MediaPlaylistComponent } from './media-playlist.component'; 4 | 5 | describe('MediaPlaylistComponent', () => { 6 | let component: MediaPlaylistComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MediaPlaylistComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MediaPlaylistComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/youtube-search/youtube-search.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { YoutubeSearchComponent } from './youtube-search.component'; 4 | 5 | describe('YoutubeSearchComponent', () => { 6 | let component: YoutubeSearchComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ YoutubeSearchComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(YoutubeSearchComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/messages-window/messages-window.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessagesWindowComponent } from './messages-window.component'; 4 | 5 | describe('MessagesWindowComponent', () => { 6 | let component: MessagesWindowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MessagesWindowComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MessagesWindowComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/nickname-prompt/nickname-prompt.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NicknamePromptComponent } from './nickname-prompt.component'; 4 | 5 | describe('NicknamePromptComponent', () => { 6 | let component: NicknamePromptComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NicknamePromptComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NicknamePromptComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/user-info-dialog/user-info-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserInfoDialogComponent } from './user-info-dialog.component'; 4 | 5 | describe('UserInfoDialogComponent', () => { 6 | let component: UserInfoDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ UserInfoDialogComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserInfoDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/* 5 | !/dist/index.html 6 | /tmp 7 | /out-tsc 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events*.json 16 | speed-measure-plugin*.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/action-prompt/action-prompt.component.html: -------------------------------------------------------------------------------- 1 |

Describe action or status

2 | 3 | 4 | 9 | 10 | {{username}}… {{actionText}} 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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 } = 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({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /src/app/chat/dialogs/media-playlist/media-playlist.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Inject} from '@angular/core'; 2 | import {ChatUser} from '../../chat-user'; 3 | import {MAT_DIALOG_DATA} from '@angular/material/dialog'; 4 | import {MediaInfo} from '../../pipes/enrich-message.pipe'; 5 | 6 | @Component({ 7 | selector: 'app-media-playlist', 8 | templateUrl: './media-playlist.component.html', 9 | styleUrls: ['./media-playlist.component.scss'] 10 | }) 11 | export class MediaPlaylistComponent { 12 | selectedMedia: MediaInfo; 13 | followingUser: ChatUser; 14 | following = new EventEmitter(); 15 | 16 | constructor( 17 | @Inject(MAT_DIALOG_DATA) public users: any, 18 | ) { } 19 | 20 | filterCallback(user: ChatUser) { 21 | return (user.playlist.length > 0); 22 | } 23 | 24 | onFollowUserClick(user) { 25 | this.followingUser = this.followingUser === user ? null : user; 26 | this.following.emit(this.followingUser); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/chat/pipes/safe.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DomSanitizer, SafeHtml, SafeStyle, SafeScript, SafeUrl, SafeResourceUrl } from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'safe' 6 | }) 7 | export class SafePipe implements PipeTransform { 8 | 9 | constructor(protected sanitizer: DomSanitizer) {} 10 | 11 | public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { 12 | switch (type) { 13 | case 'html': return this.sanitizer.bypassSecurityTrustHtml(value); 14 | case 'style': return this.sanitizer.bypassSecurityTrustStyle(value); 15 | case 'script': return this.sanitizer.bypassSecurityTrustScript(value); 16 | case 'url': return this.sanitizer.bypassSecurityTrustUrl(value); 17 | case 'resourceUrl': return this.sanitizer.bypassSecurityTrustResourceUrl(value); 18 | default: throw new Error(`Invalid safe type specified: ${type}`); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/user-info-dialog/user-info-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {ChatUser} from '../../chat-user'; 3 | import {IrcClientService} from '../../../irc-client-service/irc-client-service'; 4 | import {IrcUser} from '../../../irc-client-service/irc-user'; 5 | import {MAT_DIALOG_DATA} from '@angular/material/dialog'; 6 | 7 | @Component({ 8 | selector: 'app-user-info-dialog', 9 | templateUrl: './user-info-dialog.component.html', 10 | styleUrls: ['./user-info-dialog.component.scss'] 11 | }) 12 | export class UserInfoDialogComponent implements OnInit { 13 | u: IrcUser; 14 | isLoadingData = true; 15 | constructor( 16 | @Inject(MAT_DIALOG_DATA) public chatUser: ChatUser, 17 | private ircClientService: IrcClientService 18 | ) {} 19 | 20 | ngOnInit() { 21 | this.u = this.chatUser.user; 22 | this.ircClientService.whoisReply 23 | .subscribe(reply => this.isLoadingData = false); 24 | this.ircClientService.whois(this.chatUser.name); 25 | } 26 | 27 | onVersionClick() { 28 | this.ircClientService.ctcp(this.chatUser.name, 'VERSION'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/ng-web-irc'), 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 | -------------------------------------------------------------------------------- /src/app/services/encr-decr.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as CryptoJS from 'crypto-js'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | 8 | export class EncrDecrService { 9 | constructor() { } 10 | 11 | // The set method is use for encrypt the value. 12 | set(keys, value) { 13 | const key = CryptoJS.enc.Utf8.parse(keys); 14 | const iv = CryptoJS.enc.Utf8.parse(keys); 15 | const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(value.toString()), key, 16 | { 17 | keySize: 128 / 8, 18 | iv, 19 | mode: CryptoJS.mode.CBC, 20 | padding: CryptoJS.pad.Pkcs7 21 | }); 22 | return encrypted.toString(); 23 | } 24 | 25 | // The get method is use for decrypt the value. 26 | get(keys, value) { 27 | const key = CryptoJS.enc.Utf8.parse(keys); 28 | const iv = CryptoJS.enc.Utf8.parse(keys); 29 | const decrypted = CryptoJS.AES.decrypt(value, key, { 30 | keySize: 128 / 8, 31 | iv, 32 | mode: CryptoJS.mode.CBC, 33 | padding: CryptoJS.pad.Pkcs7 34 | }); 35 | return decrypted.toString(CryptoJS.enc.Utf8); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 g-labs (https://github.com/genielabs) 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 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/youtube-search/youtube-search.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; 2 | import {YoutubeSearchService} from '../../../services/youtube-search.service'; 3 | 4 | @Component({ 5 | selector: 'app-youtube-search', 6 | templateUrl: './youtube-search.component.html', 7 | styleUrls: ['./youtube-search.component.scss'] 8 | }) 9 | export class YoutubeSearchComponent implements OnInit { 10 | @ViewChild('inputElement', {static: true}) searchInput: ElementRef; 11 | searchText = ''; 12 | searchResults = []; 13 | minLength = 3; 14 | 15 | isLoading = false; 16 | 17 | constructor( 18 | private youTubeSearch: YoutubeSearchService 19 | ) { } 20 | 21 | ngOnInit() { 22 | setTimeout(() => { 23 | this.searchInput.nativeElement.focus(); 24 | }, 200); 25 | } 26 | 27 | onSearchClick(e) { 28 | if (this.searchText.length >= this.minLength) { 29 | this.isLoading = true; 30 | this.searchResults = []; 31 | this.youTubeSearch.search(this.searchText, (results) => { 32 | this.searchResults = results; 33 | this.isLoading = false; 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/chat/chat-user.ts: -------------------------------------------------------------------------------- 1 | import {IrcUser} from '../irc-client-service/irc-user'; 2 | import {MediaInfo} from './pipes/enrich-message.pipe'; 3 | 4 | export class ChatUser { 5 | // volatile data that will be lost if chatUser leaves the channel 6 | color: string; 7 | icon: string; 8 | constructor(public channel: string, public user: IrcUser) { 9 | // persisted data to ircClientService global users list 10 | if (user) { 11 | user.channels[channel].playlist = user.channels[channel].playlist || [] as MediaInfo[]; 12 | } 13 | } 14 | get online(): boolean { 15 | return this.user && this.user.online; 16 | } 17 | get name(): string { 18 | return this.user ? this.user.name : ''; 19 | } 20 | get flags(): string { 21 | return this.user ? this.user.channels[this.channel].flags : ''; 22 | } 23 | get playlist(): MediaInfo[] { 24 | return this.user ? this.user.channels[this.channel].playlist : []; 25 | } 26 | hasPlaylist(): boolean { 27 | return this.playlist.length > 0; 28 | } 29 | get away(): string | null { 30 | return this.user ? this.user.away : ''; 31 | } 32 | set away(message: string | undefined) { 33 | if (this.user) { 34 | this.user.away = message; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'ng-web-irc'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('ng-web-irc'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('ng-web-irc app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/nickname-prompt/nickname-prompt.component.html: -------------------------------------------------------------------------------- 1 |

User nickname

2 | 3 |
4 | 5 | 11 | Characters not allowed. 12 | Enter at least 3 characters. 13 | 14 |
15 | Registered user 16 |
17 | 18 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/app/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {PouchDBService} from './pouchdb.service'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class SettingsService { 8 | settings = { 9 | isDarkTheme: false, 10 | showColors: true 11 | }; 12 | 13 | private updateDbTimeout; 14 | 15 | constructor(private pouchDbService: PouchDBService) { } 16 | 17 | public loadSettings() { 18 | this.pouchDbService.get('settings').then(settings => { 19 | this.settings = settings; 20 | }).catch(err => { 21 | console.log('Error loading settings.', err); 22 | }); 23 | } 24 | public saveSettings() { 25 | if (this.updateDbTimeout) { 26 | if (this.updateDbTimeout !== true) { 27 | clearTimeout(this.updateDbTimeout); 28 | } 29 | this.updateDbTimeout = setTimeout(this.saveSettings.bind(this), 500); 30 | return; 31 | } 32 | this.updateDbTimeout = true; 33 | // update db 34 | this.pouchDbService.put('settings', this.settings).then((res) => { 35 | this.updateDbTimeout = false; 36 | }).catch(err => { 37 | this.updateDbTimeout = false; 38 | }); 39 | } 40 | 41 | } 42 | 43 | 44 | /** 45 | stringToColour = (str) => { 46 | let hash = 0; 47 | for (const i = 0; i < str.length; i++) { 48 | hash = str.charCodeAt(i) + ((hash << 5) - hash); 49 | } 50 | let colour = '#'; 51 | for (const i = 0; i < 3; i++) { 52 | var value = (hash >> (i * 8)) & 0xFF; 53 | colour += ('00' + value.toString(16)).substr(-2); 54 | } 55 | return colour; 56 | } 57 | */ 58 | -------------------------------------------------------------------------------- /src/app/chat/private-chat.ts: -------------------------------------------------------------------------------- 1 | import {ChatData} from './chat-data'; 2 | import {ChatManagerComponent} from './chat-manager/chat-manager.component'; 3 | import {TextFormatting} from './text-formatting'; 4 | import {ChatInfo} from './chat-info'; 5 | import {ChatUser} from './chat-user'; 6 | import {ChatMessage} from './chat-message'; 7 | 8 | export class PrivateChat extends ChatData{ 9 | private textFormatting = new TextFormatting(); 10 | user: ChatUser; 11 | 12 | constructor( 13 | chatInfo: string | ChatInfo, 14 | chatManager: ChatManagerComponent 15 | ) { 16 | super(chatInfo, chatManager); 17 | this.user = new ChatUser('private', chatManager.ircClient.getUser(this.info.name)) ; 18 | }; 19 | 20 | receive(message: ChatMessage) { 21 | message = super.receive(message); 22 | this.textFormatting.enrich(message.rendered.message) 23 | .subscribe((result) => { 24 | if (result.mediaInfo) { 25 | const existingItem = this.user.playlist 26 | .find((item) => item.url === result.mediaInfo.url); 27 | if (existingItem == null) { 28 | this.user.playlist.push(result.mediaInfo); 29 | this.chatEvent.emit({ 30 | event: 'playlist:add', 31 | user: this.user, 32 | media: result.mediaInfo 33 | }); 34 | } 35 | } 36 | message.rendered.message = result.enriched; 37 | message.rendered.musicIcon = this.user.hasPlaylist() ? 'music_video' : ''; 38 | }); 39 | return message; 40 | } 41 | getUser(name?: string) { 42 | return this.user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-web-irc", 3 | "short_name": "ng-web-irc", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/media-playlist/media-playlist.component.html: -------------------------------------------------------------------------------- 1 |

2 | music_video 3 | Shared videos 4 |

5 | 6 |
7 |
8 |

9 | {{user.name}} 10 |

11 | 17 |
18 |
19 | 20 | {{media.title}} 21 |
22 | 25 | 30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Discreet IRC client built with Angular 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/channels-list/channels-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {IrcClientService} from '../../../irc-client-service/irc-client-service'; 3 | import {IrcChannel} from '../../../irc-client-service/irc-channel'; 4 | 5 | @Component({ 6 | selector: 'app-channels-list', 7 | templateUrl: './channels-list.component.html', 8 | styleUrls: ['./channels-list.component.scss'] 9 | }) 10 | export class ChannelsListComponent implements OnInit, OnDestroy { 11 | isLoadingData = false; 12 | channelList = [] as IrcChannel[]; 13 | channelsCount = 0; 14 | channelName = ''; 15 | 16 | constructor(private ircClientService: IrcClientService) { } 17 | 18 | ngOnInit() { 19 | this.channelList = this.ircClientService.config.server.channels.slice(); 20 | this.channelList.sort((a, b) => { 21 | return +a.users < +b.users ? 1 : -1; 22 | }); 23 | this.channelsCount = this.channelList.length; 24 | this.ircClientService.channelsList.subscribe((channel: IrcChannel) => { 25 | this.channelsCount++; 26 | // channels list end signal (channel == null) 27 | if (channel == null) { 28 | const list = this.ircClientService.config.server.channels.slice(); 29 | list.sort((a, b) => { 30 | return +a.users < +b.users ? 1 : -1; 31 | }); 32 | this.channelList = list; 33 | this.isLoadingData = false; 34 | } 35 | }); 36 | } 37 | 38 | ngOnDestroy(): void { 39 | } 40 | 41 | onListDownloadClick() { 42 | this.isLoadingData = true; 43 | this.channelList.length = this.channelsCount = 0; 44 | this.ircClientService.list(); 45 | } 46 | 47 | elapsedFromListDownload() { 48 | const fromLastUpdate = (Date.now() - this.ircClientService.config.server.timestamp) / 1000; 49 | return Math.round(fromLastUpdate); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/socialmedia/youtube-video/youtube-video.component.scss: -------------------------------------------------------------------------------- 1 | .video-frame { 2 | transition: all .3s ease-in-out; 3 | transform: translate(-240px, 0); 4 | display: block; 5 | position: absolute; 6 | right: 0; 7 | top: 0; 8 | height: 80px; 9 | width: 156px; 10 | overflow: hidden; 11 | z-index: 100; 12 | border: solid 1px rgba(250, 250, 250, 0.7); 13 | } 14 | 15 | .video-frame.maximized { 16 | right: 0; 17 | top: 0; 18 | transform: translate(-240px, 0); 19 | height: 360px; 20 | width: 576px; 21 | border: 0; 22 | border-left: 6px solid; 23 | border-bottom: 6px solid; 24 | border-bottom-left-radius: 48px; 25 | } 26 | 27 | .video-frame.no-margin { 28 | transform: translate(0, 0); 29 | } 30 | 31 | .overlay { 32 | display: block; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | bottom: 0; 38 | z-index: 100; 39 | background: transparent; 40 | } 41 | 42 | .menu-button { 43 | transition: all .3s ease-in-out; 44 | transform: translate(-240px, 0); 45 | display: block; 46 | position: absolute; 47 | z-index: 100; 48 | top: 0; 49 | right: 558px; 50 | box-shadow: none !important; 51 | } 52 | .menu-button.no-margin { 53 | transform: translate(0, 0); 54 | } 55 | .hidden { 56 | display: none; 57 | } 58 | 59 | @media only screen and (max-device-width: 1024px) { 60 | .video-frame.maximized { 61 | height: 300px; 62 | width: 480px; 63 | } 64 | .menu-button { 65 | right: 462px; 66 | } 67 | } 68 | 69 | @media only screen and (max-device-width: 768px) { 70 | .video-frame.maximized { 71 | height: 200px; 72 | width: 320px; 73 | } 74 | .menu-button { 75 | right: 302px; 76 | } 77 | } 78 | 79 | @media only screen and (max-device-width: 600px) { 80 | .video-frame { 81 | top: 6px; 82 | } 83 | } 84 | 85 | @media only screen and (max-device-height: 452px) { 86 | .video-frame.maximized { 87 | height: 128px; 88 | width: 252px; 89 | } 90 | .menu-button { 91 | right: 302px; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/socialmedia/youtube-video/youtube-video.component.html: -------------------------------------------------------------------------------- 1 | 8 |
9 |
10 |
13 |
14 | 18 | 19 | 20 | 24 | 28 | 32 | 36 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/services/pouchdb.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, EventEmitter } from '@angular/core'; 2 | import PouchDB from 'pouchdb'; 3 | import { EncrDecrService } from './encr-decr.service'; 4 | 5 | @Injectable() 6 | export class PouchDBService { 7 | 8 | private isInstantiated: boolean; 9 | private database: any; 10 | private listener: EventEmitter = new EventEmitter(); 11 | 12 | public constructor(private cryptoService: EncrDecrService) { 13 | if (!this.isInstantiated) { 14 | this.database = new PouchDB('Discreet'); 15 | this.isInstantiated = true; 16 | } 17 | } 18 | 19 | public fetch() { 20 | return this.database.allDocs({include_docs: true}); 21 | } 22 | 23 | public get(id: string) { 24 | return this.database.get(id); 25 | } 26 | 27 | public put(id: string, document: any) { 28 | document._id = id; 29 | return this.get(id).then(result => { 30 | document._rev = result._rev; 31 | return this.database.put(document); 32 | }, error => { 33 | if(error.status == '404') { 34 | return this.database.put(document); 35 | } else { 36 | return new Promise((resolve, reject) => { 37 | reject(error); 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | public sync(remote: string) { 44 | const remoteDatabase = new PouchDB(remote); 45 | this.database.sync(remoteDatabase, { 46 | live: true 47 | }).on('change', change => { 48 | this.listener.emit(change); 49 | }).on('error', error => { 50 | console.error(JSON.stringify(error)); 51 | }); 52 | } 53 | 54 | public getChangeListener() { 55 | return this.listener; 56 | } 57 | 58 | encrypt(unencrypted: string) { 59 | return this.cryptoService.set('1a3b5c$#@$^@MARS', unencrypted); 60 | } 61 | decrypt(encrypted: string) { 62 | return this.cryptoService.get('1a3b5c$#@$^@MARS', encrypted); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/user-info-dialog/user-info-dialog.component.html: -------------------------------------------------------------------------------- 1 |

2 | info 3 | {{u.name}} 4 |

5 | 6 | 7 |
{{u.whois.address[0]}}@{{u.whois.address[1]}}
8 |
9 | Connected 10 | {{ (u.whois.time[1] | amFromUnix) | amLocale:'it' | amCalendar }}, 11 | inactive since 12 | {{ +u.whois.time[0] | amDuration:'seconds' }} 13 | ({{ u.whois.identified }}). 14 |
15 | 16 |
{{ u.whois.channels }}
17 | 18 |
{{ u.whois.server[0] }} (SSL)
19 |
{{ u.whois.server[1] }}
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 63 | -------------------------------------------------------------------------------- /src/app/chat/dialogs/youtube-search/youtube-search.component.html: -------------------------------------------------------------------------------- 1 |

2 | music_video 3 | Search video 4 | 7 |

8 | 9 |
10 |
11 | 12 | {{media.title}} 13 |
14 |
15 | 18 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 | 33 | 36 | 37 | Enter at least 3 characters. 38 | 39 | 40 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/app/splash-screen/splash-screen.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Discreet chat_bubble

5 |
6 | 7 | Server 8 | 11 | 12 | {{server.name}} 13 | 14 | 15 | 16 |
17 |
18 | 19 | 25 | Characters not allowed. 26 | Enter at least 3 characters. 27 | 28 |
29 | Registered user 30 |
31 | 32 | 35 | 36 |
37 |
38 | 42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 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-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-use-before-declare": true, 64 | "no-var-requires": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "object-literal-sort-keys": false, 70 | "ordered-imports": false, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "trailing-comma": false, 76 | "no-conflicting-lifecycle": true, 77 | "no-host-metadata-property": true, 78 | "no-input-rename": true, 79 | "no-inputs-metadata-property": true, 80 | "no-output-native": true, 81 | "no-output-on-prefix": true, 82 | "no-output-rename": true, 83 | "no-outputs-metadata-property": true, 84 | "template-banana-in-box": true, 85 | "template-no-negated-async": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } -------------------------------------------------------------------------------- /src/app/chat/dialogs/channels-list/channels-list.component.html: -------------------------------------------------------------------------------- 1 |

2 | Channels 3 |
4 |  ({{ channelsCount }}) 5 |
6 | people 7 |

8 | 9 | 12 |
13 |
17 | {{ channel.name }} 18 | {{channel.users}} 19 |
20 |
21 |
22 | Loading... 23 |
24 | 25 | people 26 |   27 | 28 | 32 | 35 | 36 |
37 | 38 | 42 | 45 |
46 |
47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-web-irc", 3 | "version": "0.1.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 | "build-i18n": "for lang in en it; do ng build --output-path=dist/$lang --aot --prod --base-href /chat/$lang/ --i18n-file=src/locale/messages.$lang.xlf --i18n-format=xlf --i18n-locale=$lang; done" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^11.2.9", 16 | "@angular/cdk": "^11.2.8", 17 | "@angular/common": "^11.2.9", 18 | "@angular/compiler": "^11.2.9", 19 | "@angular/core": "^11.2.9", 20 | "@angular/elements": "^11.2.9", 21 | "@angular/flex-layout": "^11.0.0-beta.33", 22 | "@angular/forms": "^11.2.9", 23 | "@angular/material": "^11.2.8", 24 | "@angular/platform-browser": "^11.2.9", 25 | "@angular/platform-browser-dynamic": "^11.2.9", 26 | "@angular/router": "^11.2.9", 27 | "@angular/service-worker": "^11.2.9", 28 | "@ctrl/ngx-emoji-mart": "^1.0.2", 29 | "@types/crypto-js": "^3.1.43", 30 | "buffer": "^5.4.3", 31 | "classlist.js": "^1.1.20150312", 32 | "core-js": "^3.3.6", 33 | "crypto-js": "^3.1.9-1", 34 | "fetch-polyfill": "^0.8.2", 35 | "hammerjs": "^2.0.8", 36 | "intl": "^1.2.5", 37 | "irc-formatting": "^1.0.0-rc3", 38 | "loadash": "^1.0.0", 39 | "moment": "^2.29.2", 40 | "ngx-device-detector": "^1.3.14", 41 | "ngx-moment": "^3.4.0", 42 | "ngx-scroll-event": "^1.0.8", 43 | "ngx-tribute": "^1.5.0", 44 | "pouchdb": "^7.0.0", 45 | "rxjs": "^6.6.7", 46 | "smoothscroll-polyfill": "^0.4.4", 47 | "tributejs": "^4.0.0", 48 | "tslib": "^1.10.0", 49 | "web-animations-js": "^2.3.2", 50 | "youtube-search": "^1.1.4", 51 | "zone.js": "~0.10.3" 52 | }, 53 | "devDependencies": { 54 | "@angular-devkit/build-angular": "^0.1102.8", 55 | "@angular/cli": "^11.2.8", 56 | "@angular/compiler-cli": "^11.2.9", 57 | "@angular/localize": "^11.2.13", 58 | "@types/jasmine": "~3.6.0", 59 | "@types/jasminewd2": "~2.0.3", 60 | "codelyzer": "^6.0.0", 61 | "jasmine-core": "~3.6.0", 62 | "jasmine-spec-reporter": "~5.0.0", 63 | "karma": "~6.1.1", 64 | "karma-chrome-launcher": "~3.1.0", 65 | "karma-coverage-istanbul-reporter": "~3.0.2", 66 | "karma-jasmine": "~4.0.0", 67 | "karma-jasmine-html-reporter": "^1.5.0", 68 | "protractor": "~7.0.0", 69 | "ts-node": "~8.3.0", 70 | "tslint": "^6.1.3", 71 | "typescript": "^4.1.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/splash-screen/splash-screen.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, OnInit, Output} from '@angular/core'; 2 | import {LoginInfo} from '../irc-client-service/login-info'; 3 | import {IrcClientService} from '../irc-client-service/irc-client-service'; 4 | import {ActivatedRoute} from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-splash-screen', 8 | templateUrl: './splash-screen.component.html', 9 | styleUrls: ['./splash-screen.component.scss'] 10 | }) 11 | export class SplashScreenComponent implements OnInit { 12 | @Output() connectRequest = new EventEmitter(); 13 | serverId: string; 14 | nick = 'Discreet-' + Math.ceil(Math.random() * 1000); 15 | password = ''; 16 | 17 | singleServerMode = false; 18 | autoJoin = [] as string[]; 19 | 20 | constructor( 21 | public ircClientService: IrcClientService, 22 | private route: ActivatedRoute 23 | ) { } 24 | 25 | ngOnInit() { 26 | this.route.queryParams.subscribe(params => { 27 | const serverId = params.s; 28 | let channelName = params.c; 29 | if (typeof channelName === 'string') { 30 | channelName = [ channelName ]; 31 | } 32 | if (serverId) { 33 | const server = this.ircClientService.serverList 34 | .find(s => s.id === serverId); 35 | if (server) { 36 | this.singleServerMode = true; 37 | this.serverId = serverId; 38 | if (channelName) { 39 | this.autoJoin = channelName.map((s) => s.replace('!', '#')); 40 | } 41 | } 42 | } 43 | }); 44 | this.loadConfiguration(); 45 | } 46 | 47 | onConnectClick() { 48 | this.connectRequest.emit({ 49 | server: this.ircClientService.serverList 50 | .find(server => server.id === this.serverId), 51 | nick: this.nick, 52 | password: this.password, 53 | autoJoin: this.autoJoin 54 | } as LoginInfo); 55 | } 56 | 57 | onServerSelectChange(e) { 58 | this.serverId = e.value; 59 | this.loadConfiguration(); 60 | } 61 | 62 | filterCallback(server) { 63 | return !server.hidden; 64 | } 65 | 66 | loadConfiguration() { 67 | this.ircClientService.loadConfiguration(this.serverId, (cfg, err) => { 68 | cfg = cfg || this.ircClientService.config; 69 | if (this.serverId) { 70 | this.ircClientService.config.server = this.ircClientService.serverList 71 | .find((s) => s.id === this.serverId); 72 | } 73 | this.nick = cfg.nick || this.nick; 74 | this.password = cfg.password; 75 | this.serverId = cfg.server.id; 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: absolute; 4 | left: 0; top: 0; right: 0; bottom: 0; 5 | overflow: hidden; 6 | } 7 | 8 | .loading-message { 9 | position: fixed; 10 | left: 0; top: 0; right: 0; bottom: 0; 11 | background: rgba(0, 0, 0, 0.3); 12 | z-index: 1000; /* 1001 is used for dialogs */ 13 | } 14 | 15 | .content { 16 | position: absolute; 17 | top: 0; left: 0; right: 0; bottom: 0; 18 | overflow: hidden; 19 | } 20 | 21 | .left-panel { 22 | border: 0; 23 | border-right: solid 1px; 24 | padding: 8px; 25 | 26 | & .mat-flat-button { 27 | background: transparent; 28 | width: 100%; 29 | margin-top: 2px !important; 30 | margin-bottom: 2px !important; 31 | text-align: left; /* IE / Edge compatibility */ 32 | text-align: start; 33 | 34 | & .mat-icon { 35 | margin-right: 8px; 36 | } 37 | 38 | } 39 | 40 | & h4 { 41 | margin: 12px 12px 6px; 42 | } 43 | 44 | & label { 45 | display: block; 46 | font-weight: 600; 47 | margin: 36px 12px 12px; 48 | } 49 | 50 | & .mat-checkbox { 51 | display: block; 52 | margin-top: 8px; 53 | margin-bottom: 8px; 54 | margin-left: 20px; 55 | } 56 | 57 | } 58 | 59 | .right-toolbar { 60 | padding-right: 4px; 61 | .icon-big.mat-icon-button { 62 | margin-left: 16px; 63 | } 64 | .mat-badge-medium.mat-badge-above .mat-badge-content { 65 | top: 0 !important; 66 | } 67 | } 68 | 69 | mat-toolbar { 70 | position: relative; 71 | z-index: 100; 72 | .title { 73 | overflow: hidden; 74 | } 75 | .title-text { 76 | padding: 6px 16px; 77 | border-radius: 24px; 78 | border: solid 2px; 79 | background-color: rgba(255,255,255,0.2); 80 | } 81 | } 82 | 83 | mat-sidenav { 84 | width: 240px; 85 | } 86 | /* 87 | mat-sidenav { 88 | height: 100vh; 89 | max-height: 100vh; 90 | } 91 | */ 92 | mat-sidenav-container { 93 | height: calc(100vh - 64px); 94 | } 95 | mat-sidenav-content { 96 | height: 100%; 97 | max-height: 100%; 98 | } 99 | 100 | mat-drawer-content { 101 | margin-left: 200px; 102 | } 103 | .mat-drawer-side.mat-drawer-end { 104 | border: 0; 105 | } 106 | .mat-drawer-inner-container { 107 | overflow-x: hidden !important; 108 | } 109 | 110 | .menu-button { 111 | transform: translateX(-16px); 112 | } 113 | /* break point for material toolbar (from 64px height to 56px) */ 114 | @media only screen and (max-device-width: 599px) { // small screen 115 | mat-sidenav-container { 116 | height: calc(100vh - 56px); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/app/chat/public-chat.ts: -------------------------------------------------------------------------------- 1 | import {ChatData} from './chat-data'; 2 | import {ChatUser} from './chat-user'; 3 | import {ChatMessage} from './chat-message'; 4 | import {TextFormatting} from './text-formatting'; 5 | 6 | export class PublicChat extends ChatData { 7 | private textFormatting = new TextFormatting(); 8 | 9 | topic = ''; 10 | mode: string; 11 | users: ChatUser[] = [] as ChatUser[]; 12 | userStatus = { 13 | // kicked from channel 14 | kicked: false, 15 | // banned from channel 16 | banned: false, 17 | // only invited users can join 18 | invite: false, 19 | // only registered users can join 20 | registered: false 21 | }; 22 | preferences: any = { 23 | showChannelActivity: true, 24 | showChannelActivityToggle() { 25 | this.showChannelActivity = !this.showChannelActivity; 26 | } 27 | }; 28 | 29 | receive(message: ChatMessage) { 30 | message = super.receive(message); 31 | const senderUser = this.getUser(message.sender); 32 | this.textFormatting.enrich(message.rendered.message) 33 | .subscribe((result) => { 34 | if (result.mediaInfo) { 35 | if (senderUser) { 36 | const existingItem = senderUser.playlist 37 | .find((item) => item.url === result.mediaInfo.url); 38 | if (existingItem == null) { 39 | senderUser.playlist.push(result.mediaInfo); 40 | this.chatEvent.emit({ 41 | event: 'playlist:add', 42 | user: senderUser, 43 | media: result.mediaInfo 44 | }); 45 | } 46 | } 47 | } 48 | message.rendered.message = result.enriched; 49 | if (senderUser) { 50 | message.rendered.musicIcon = senderUser.hasPlaylist() ? 'music_video' : ''; 51 | } 52 | }); 53 | 54 | message.rendered.flagsIconColor = this.getUserColor(message.sender); 55 | message.rendered.flagsIconName = this.getUserIcon(message.sender); 56 | if (message.rendered.flagsIconName === 'person') { 57 | // do not show standard use icon in message buffer 58 | message.rendered.flagsIconName = null; 59 | } 60 | return message; 61 | } 62 | 63 | getUser(name: string) { 64 | return this.users.find((u) => u.name === name); 65 | } 66 | getUserIcon(name: string) { 67 | const u = this.getUser(name); 68 | return u != null && this.manager().getIcon(u.flags); 69 | } 70 | getUserColor(name: string) { 71 | const u = this.getUser(name); 72 | return u != null && this.manager().getColor(u.flags); 73 | } 74 | reset() { 75 | this.users.length = 0; 76 | this.userStatus = { 77 | kicked: false, 78 | banned: false, 79 | invite: false, 80 | registered: false 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/chat/messages-window/messages-window.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; 2 | import {ScrollEvent} from 'ngx-scroll-event'; 3 | import {ChatMessageType, ChatMessage} from '../chat-message'; 4 | import {PrivateChat} from '../private-chat'; 5 | import {PublicChat} from '../public-chat'; 6 | 7 | import * as smoothScroll from '../../../../node_modules/smoothscroll-polyfill'; 8 | import {TextFormatting} from '../text-formatting'; 9 | 10 | @Component({ 11 | selector: 'app-messages-window', 12 | templateUrl: './messages-window.component.html', 13 | styleUrls: ['./messages-window.component.scss'] 14 | }) 15 | export class MessagesWindowComponent implements OnInit { 16 | private textFormatting = new TextFormatting(); 17 | private scrollTimeout; 18 | 19 | @ViewChild('chatBuffer', {static: true}) 20 | chatBuffer: ElementRef; 21 | 22 | @Input() 23 | boundChat: any; 24 | @Output() 25 | mediaUrlClick = new EventEmitter(); 26 | 27 | MessageType = ChatMessageType; 28 | isLastMessageVisible = true; 29 | 30 | constructor() { 31 | smoothScroll.polyfill(); 32 | } 33 | 34 | ngOnInit() { 35 | } 36 | 37 | handleScroll(event: ScrollEvent) { 38 | // console.log('scroll occurred', event.originalEvent); 39 | if (event.isReachingBottom) { 40 | this.isLastMessageVisible = true; 41 | this.boundChat.stats.messages.new = 0; 42 | } else if (event.isReachingTop) { 43 | this.isLastMessageVisible = false; 44 | } else { 45 | // scrolling 46 | this.isLastMessageVisible = false; 47 | } 48 | // if (event.isWindowEvent) { 49 | // console.log(`This event is fired on Window not on an element.`); 50 | // } 51 | } 52 | 53 | onMessageClick(e) { 54 | // Handle click on Anchor elements (YouTube and other external links) 55 | if (e.target.tagName === 'A') { 56 | e.preventDefault(); 57 | this.mediaUrlClick.emit({ id: e.target.dataset.id, link: e.target.dataset.link, element: e.target }); 58 | } 59 | } 60 | 61 | bind(chat: PrivateChat | PublicChat) { 62 | this.boundChat = chat; 63 | this.scrollLast(true); 64 | } 65 | 66 | isServiceMessage(msg) { 67 | return msg.type === ChatMessageType.JOIN 68 | || msg.type === ChatMessageType.PART 69 | || msg.type === ChatMessageType.QUIT 70 | || msg.type === ChatMessageType.KICK; 71 | } 72 | 73 | scrollLast(force?: boolean, soft?: boolean) { 74 | if (this.scrollTimeout != null) { 75 | clearTimeout(this.scrollTimeout); 76 | } 77 | const el: HTMLElement = this.chatBuffer.nativeElement; 78 | if (force && !soft) { 79 | el.style['scroll-behavior'] = 'initial'; 80 | this.scrollTimeout = setTimeout(() => { 81 | el.scrollTo(0, el.scrollHeight); 82 | el.style['scroll-behavior'] = 'smooth'; 83 | }, 10); 84 | } else if (this.isLastMessageVisible || force) { 85 | this.scrollTimeout = setTimeout(() => { el.scrollTo(0, el.scrollHeight); }, 10); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discreet (codename: ng-web-irc) 2 | 3 | 4 | 5 | Discreet is a self-hosted anonymous chat client based on the IRC protocol implemented over a websocket connection (WebIRC). 6 | Written using [Angular](https://angular.io/) and [Angular-Material](https://material.angular.io/). 7 | 8 | Features in brief: 9 | - responsive and adaptive layout that works both on desktop and mobile 10 | - supports different locales 11 | - emoji and IRC color codes decoding 12 | - nick auto-complete (by start typing `@` and a few initial letters) 13 | - public/private message notifications 14 | - automatic media urls parsing (e.g. gets and displays YouTube video info) 15 | - automatic video playlists with integrated video player 16 | - integrated YouTube video search 17 | - dark theme 18 | 19 | Discreet has been tested with [InspIRCD](https://github.com/inspircd/inspircd) with *websocket* module enabled. 20 | You can change server connection properties by editing the file `src/assets/server-list.json`. 21 | 22 | **PLEASE NOTE** 23 | 24 | The file `src/app/irc-client-service/irc-client-service.ts` only contains a basic and draft implementation of IRC client protocol. 25 | Full protocol specifications are available from [IRCv3 Specifications](https://ircv3.net/irc/). 26 | 27 | Due to a bug in the CLI, you might encounter a "Javascript heat out of memory" while running `ng serve`. This can be fixed by using the following command instead: 28 | 29 | `node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve` 30 | 31 | 32 | ## Development server 33 | 34 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 35 | 36 | ### Serving files in for a specific locale 37 | 38 | Run `ng serve --configuration=` (eg. `ng serve --configuration=it` for italian). 39 | 40 | #### Implemented locales 41 | 42 | - English 43 | - Italian 44 | 45 | ## Code scaffolding 46 | 47 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 48 | 49 | ## Build 50 | 51 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/en` directory. Use the `--prod` flag for a production build. 52 | 53 | ### Building with locale support 54 | 55 | Run `npm run build-i18n`. The build artifacts will be stored in the `dist/` directory. 56 | The `dist/index.html` file will auto detect client language and redirect the browser to the current 57 | locale folder if supported (eg. `dist/it` for italian), otherwise will fallback to the default language (english). 58 | 59 | ## Running unit tests 60 | 61 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 62 | 63 | ## Running end-to-end tests 64 | 65 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 66 | 67 | ## Further help 68 | 69 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 70 | -------------------------------------------------------------------------------- /src/app/chat/text-formatting.ts: -------------------------------------------------------------------------------- 1 | import {Observable, Observer} from 'rxjs'; 2 | import {MediaInfo} from './pipes/enrich-message.pipe'; 3 | import {HttpClient, HttpXhrBackend} from '@angular/common/http'; 4 | 5 | export class EnrichmentResult { 6 | enriched: string; 7 | mediaInfo?: MediaInfo; 8 | } 9 | 10 | export class TextFormatting { 11 | static mediaUrlsCache: MediaInfo[] = []; 12 | 13 | constructor() {} 14 | 15 | enrich(value): Observable { 16 | const httpClient = new HttpClient(new HttpXhrBackend({ build: () => new XMLHttpRequest() })); 17 | const text = this.createTextLinks(value); 18 | return new Observable((observer: Observer) => { 19 | observer.next({ enriched: text.replaced }); 20 | text.urls.forEach((url) => { 21 | const cached = TextFormatting.mediaUrlsCache.find((v) => v.originalUrl === url || v.url === url); 22 | if (cached != null) { 23 | observer.next({ 24 | enriched: text.replaced.replace(`[${url}]`, `${cached.title} (${cached.provider_name})`), 25 | mediaInfo: cached 26 | }); 27 | return; 28 | } 29 | const o = httpClient.get(`https://noembed.com/embed?url=${url}`); 30 | o.subscribe((res: MediaInfo) => { 31 | if (res && res.title) { 32 | res.originalUrl = url; 33 | TextFormatting.mediaUrlsCache.push(res); 34 | observer.next({ 35 | enriched: text.replaced.replace(`[${url}]`, `${res.title} (${res.provider_name})`), 36 | mediaInfo: res 37 | }); 38 | } else { 39 | observer.next({ 40 | enriched: text.replaced.replace(`[${url}]`, url) 41 | }); 42 | } 43 | }, (err) => { 44 | // TODO: ... 45 | observer.next({ 46 | enriched: text.replaced.replace(`[${url}]`, url) 47 | }); 48 | }, () => { 49 | observer.complete(); 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | createTextLinks(text: string) { 56 | const urls: string[] = []; 57 | const replaced = (text || '').replace( 58 | /([^\S]|^)(((https?:\/\/)|(www\.*\.*))([-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)))/gi, 59 | (match, space, url) => { 60 | let hyperlink = url; 61 | if (!hyperlink.match('^https?:\/\/')) { 62 | hyperlink = 'http://' + hyperlink; 63 | } 64 | urls.push(url); 65 | let mediaUrl = hyperlink; 66 | if (hyperlink.indexOf('/youtube.com/') !== -1 67 | || hyperlink.indexOf('.youtube.com/') !== -1 68 | || hyperlink.indexOf('/youtu.be/') !== -1 69 | || hyperlink.indexOf('.youtu.be/') !== -1 70 | ) { 71 | let videoId = ''; 72 | if (hyperlink.indexOf('v=') === -1) { 73 | videoId = hyperlink.substring(hyperlink.lastIndexOf('/') + 1); 74 | } else { 75 | videoId = hyperlink.split('v=')[1]; 76 | const ampersandPosition = videoId.indexOf('&'); 77 | if (ampersandPosition !== -1) { 78 | videoId = videoId.substring(0, ampersandPosition); 79 | } 80 | } 81 | mediaUrl = `[${url}]`; 82 | } else { 83 | mediaUrl = `[${url}]`; 84 | } 85 | return space + mediaUrl; 86 | } 87 | ); 88 | return { replaced, urls }; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/themes/material-theme.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | //@import "~@angular/material/prebuilt-themes/indigo-pink.css"; 3 | //@import "~@angular/material/prebuilt-themes/pink-bluegrey.css"; 4 | 5 | /*@import url('https://fonts.googleapis.com/css?family=Nova+Mono&display=swap');*/ 6 | /*@import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,700&display=swap&subset=latin-ext');*/ 7 | /*@import url('https://fonts.googleapis.com/css?family=Space+Mono:400,700&display=swap&subset=latin-ext');*/ 8 | 9 | // Import material theming functions 10 | @import '~@angular/material/theming'; 11 | 12 | // Typography 13 | $custom-typography: mat-typography-config( 14 | $font-family: 'Hind Madurai' 15 | ); 16 | @include mat-core($custom-typography); 17 | 18 | /* ======== Angular material custom themes ======== */ 19 | 20 | // Default Theme 21 | $default-theme-primary: mat-palette($mat-deep-purple); 22 | $default-theme-accent: mat-palette($mat-pink, A200, A100, A400); 23 | $default-theme-warn: mat-palette($mat-red); 24 | $default-theme: mat-light-theme($default-theme-primary, $default-theme-accent, $default-theme-warn); 25 | 26 | @include angular-material-theme($default-theme); 27 | 28 | // color variables (will be available in the whole project) 29 | // TODO: remove these 3 use mat-color instead 30 | //$primary: mat-color($default-theme-primary); 31 | $accent: mat-color($default-theme-accent); 32 | //$warn: mat-color($default-theme-warn); 33 | 34 | $positive: mat-palette($mat-green); 35 | $negative: mat-palette($mat-red); 36 | $notice: mat-palette($mat-orange); 37 | 38 | .elevation-background { 39 | } 40 | .theme-color-primary { 41 | color: mat-color($default-theme-primary); 42 | } 43 | 44 | .theme-color-positive { 45 | color: mat-color($positive); 46 | } 47 | .theme-color-negative { 48 | color: mat-color($negative); 49 | } 50 | .theme-color-notice { 51 | color: mat-color($notice); 52 | } 53 | .theme-background-accent { 54 | background-color: mat-color($default-theme-accent); 55 | } 56 | .theme-border-primary { 57 | border-color: mat-color($default-theme-primary, 300, 0.5) !important; 58 | } 59 | 60 | /* 61 | $primary: mat-palette($mat-indigo); 62 | $accent: mat-palette($mat-pink, A200, A100, A400); 63 | $warn: mat-palette($mat-red); 64 | $theme: mat-light-theme($primary, $accent, $warn); 65 | @include angular-material-theme($theme); 66 | */ 67 | 68 | // Dark Theme 69 | .dark-theme { 70 | color: #bbb; 71 | $dark-background-color: #444; //map_get($mat-dark-theme-background, 50); 72 | $dark-primary: mat-palette($mat-green, 400); 73 | $dark-accent: mat-palette($mat-amber, A400, A100, A700); 74 | $dark-warn: mat-palette($mat-red); 75 | $dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn); 76 | 77 | // Insert custom background color 78 | $background: map-get($dark-theme, background); 79 | $background: map_merge($background, (background: $dark-background-color)); 80 | $dark-theme: map_merge($dark-theme, (background: $background)); 81 | 82 | @include angular-material-theme($dark-theme); 83 | 84 | $dark-positive: mat-palette($mat-lime); 85 | $dark-negative: mat-palette($mat-pink); 86 | $dark-notice: mat-palette($mat-yellow); 87 | 88 | .elevation-background { 89 | background-color: #333 !important; 90 | } 91 | .theme-color-primary { 92 | color: mat-color($dark-primary) !important; 93 | } 94 | .theme-color-positive { 95 | color: mat-color($dark-positive); 96 | } 97 | .theme-color-negative { 98 | color: mat-color($dark-negative); 99 | } 100 | .theme-color-notice { 101 | color: mat-color($dark-notice); 102 | } 103 | .theme-background-accent { 104 | color: #000 !important; 105 | background-color: mat-color($dark-accent); 106 | } 107 | .theme-border-primary { 108 | border-color: mat-color($dark-primary, 300, 0.5) !important; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/app/chat/messages-window/messages-window.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | .top-shadow { 9 | position: absolute; 10 | height: 48px; 11 | top:-48px; 12 | width: 100%; 13 | z-index: 1; 14 | } 15 | 16 | .mat-menu-trigger { 17 | cursor: pointer; 18 | } 19 | 20 | .message-buffer { 21 | display: block; 22 | overflow-x: hidden; 23 | overflow-y: auto; 24 | padding: 8px; 25 | font-size: 105%; 26 | } 27 | .message-buffer:first-child { 28 | display: block; 29 | margin-top: auto; 30 | } 31 | 32 | .margin-top { 33 | margin-top: 24px !important; 34 | } 35 | .margin-bottom { 36 | margin-bottom: 24px !important; 37 | } 38 | 39 | .message { 40 | display: block; 41 | } 42 | .message-sender { 43 | opacity: 1.0; 44 | font-weight: 500; 45 | } 46 | .message-sender .icon { 47 | margin-left: 4px; 48 | margin-right: 4px; 49 | font-size: 80%; 50 | line-height: 120%; 51 | opacity: 0.6; 52 | } 53 | .message-sender span { 54 | word-break: keep-all; 55 | white-space: nowrap; 56 | font-weight: 600; 57 | opacity: 0.75; 58 | margin-right: 6px; 59 | margin-bottom: 2px; 60 | font-size: 105%; 61 | cursor: pointer; 62 | } 63 | .message-sender .info { 64 | cursor: help; 65 | } 66 | 67 | /* ACTION MESSAGE */ 68 | .message-sender.action { 69 | text-align: center; 70 | } 71 | .message-sender.action .message { 72 | font-weight: 400 !important; 73 | } 74 | .message-sender.action .icon { 75 | margin-left: 0 !important; 76 | margin-right: 4px !important; 77 | margin-bottom: 0; 78 | font-size: 120% !important; 79 | line-height: 120% !important; 80 | opacity: 1 !important; 81 | } 82 | 83 | 84 | 85 | /* JOIN/PART MESSAGE */ 86 | .message-sender.service { 87 | margin-top: 0; 88 | margin-bottom: 0; 89 | font-weight: 400; 90 | opacity: 0.5; 91 | font-size: 85%; 92 | } 93 | .message-sender.service .message { 94 | font-weight: 400 !important; 95 | } 96 | .message-sender.service .icon { 97 | margin-left: 0 !important; 98 | margin-right: 8px !important; 99 | margin-bottom: 4px; 100 | font-size: 120% !important; 101 | font-weight: 800; 102 | line-height: 150% !important; 103 | opacity: 1 !important; 104 | } 105 | .message-sender.service .message-date { 106 | font-size: 95%; 107 | opacity: 0.5; 108 | } 109 | 110 | 111 | 112 | .message-body { 113 | font-family: 'Fira Code', monospace; 114 | /*font-family: 'Space Mono', monospace;*/ 115 | /*font-family: 'Fira Mono', monospace !important;*/ 116 | /*font-family: 'Nova Mono', monospace;*/ 117 | /*font-family: 'Delius', monospace;*/ 118 | white-space: pre-wrap; 119 | line-height: 150%; 120 | vertical-align: middle; 121 | word-break: break-word; 122 | margin-left: 12px; 123 | margin-right: 12px; 124 | } 125 | 126 | .local-user { 127 | font-weight: bolder; 128 | opacity: 1 !important; 129 | filter: invert(0.7); 130 | } 131 | 132 | .message-date { 133 | padding: 4px; 134 | margin-left: 8px; 135 | font-size: 75%; 136 | font-family: monospace; 137 | opacity: 0.3; 138 | width: 100%; 139 | text-align: right; /* IE / Edge compatibility */ 140 | text-align: end; 141 | } 142 | 143 | .message-notification { 144 | position: absolute; 145 | bottom: 0; 146 | margin-left: auto; 147 | margin-right: auto; 148 | text-align: center; 149 | color: white; 150 | right: 0; 151 | left: 0; 152 | width: 320px; 153 | font-size: 16px; 154 | border-radius: 12px; 155 | margin-bottom: 24px; 156 | padding: 6px; 157 | cursor: pointer; 158 | } 159 | 160 | .message-notification mat-icon { 161 | margin-left: 32px; 162 | margin-right: 32px; 163 | } 164 | 165 | @media only screen and (max-device-width: 1023px) { 166 | /* Styles */ 167 | .margin-top { 168 | margin-top: 16px !important; 169 | } 170 | .margin-bottom { 171 | margin-bottom: 16px !important; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/app/chat/chat-data.ts: -------------------------------------------------------------------------------- 1 | import {ChatInfo} from './chat-info'; 2 | import {ChatManagerComponent} from './chat-manager/chat-manager.component'; 3 | import {ChatMessage, ChatMessageType} from './chat-message'; 4 | import ircFormatter from 'irc-formatting'; 5 | import {EventEmitter} from '@angular/core'; 6 | 7 | export class ChatData { 8 | hidden = false; 9 | messages: ChatMessage[] = []; 10 | stats = new ChatStats(); 11 | timestamp = Date.now(); 12 | preferences: any = {}; 13 | input = { 14 | text: '', 15 | selectionStart: 0, 16 | selectionEnd: 0 17 | }; 18 | userStatus: any; 19 | serviceMessage: ChatMessage; 20 | showColors = false; 21 | chatEvent = new EventEmitter(); 22 | 23 | readonly info: ChatInfo; 24 | private bufferMaxLines = 300; 25 | 26 | constructor( 27 | target: string | ChatInfo, 28 | private chatManager: ChatManagerComponent, 29 | ) { 30 | if (target instanceof ChatInfo) { 31 | this.info = target; 32 | } else { 33 | this.info = new ChatInfo(target); 34 | } 35 | } 36 | 37 | // Public methods 38 | target(): ChatInfo { 39 | return this.info; 40 | } 41 | manager() { 42 | return this.chatManager; 43 | } 44 | send(message: string) { 45 | const name = this.target().name; 46 | // deliver message to this chat 47 | this.chatManager.client().send(name, message); 48 | // Add the outgoing message to the buffer as well 49 | this.receive({ 50 | type: ChatMessageType.MESSAGE, 51 | sender: this.chatManager.client().nick(), 52 | target: name, 53 | message, 54 | rendered: {}, 55 | isLocal: true 56 | }); 57 | } 58 | receive(message: ChatMessage): ChatMessage { 59 | 60 | message = Object.assign(new ChatMessage(), message); 61 | if (message.message.startsWith('\x01ACTION ') && message.message.endsWith('\x01')) { 62 | message.type = ChatMessageType.ACTION; 63 | message.message = message.message.slice(8, -1); 64 | } 65 | 66 | if (this.manager().settingsService.settings.showColors) { 67 | // render color codes 68 | message.rendered.message = ircFormatter.renderHtml(message.message); 69 | } else { 70 | // strip color codes 71 | message.rendered.message = ircFormatter.strip(message.message); 72 | } 73 | 74 | // find chatUser nick in sentence and make it bold 75 | let nickMatched = false; 76 | const nick = this.chatManager.client().nick(); 77 | const replacer = new RegExp(`(^|\\b)${nick}(?=\\W|\\w+|$)`, 'ig'); 78 | message.rendered.message = message.rendered.message.replace(replacer, (match) => { 79 | nickMatched = true; 80 | return `${nick}`; 81 | }); 82 | if (!this.preferences.disableNotifications && nickMatched && this.info !== this.chatManager.chat().info) { 83 | this.chatManager.notify(message.sender, `[${this.info.name}] ${message.message}`, this.info); 84 | } 85 | 86 | // keep only 'bufferMaxLines' messages 87 | if (this.messages.length === this.bufferMaxLines) { 88 | this.messages.shift(); 89 | } 90 | // add incoming messages to the message buffer 91 | this.messages.push(message); 92 | // if this is not the current chat, then increase 93 | // the number of unread messages 94 | if (!this.preferences.disableNotifications && message.type === ChatMessageType.MESSAGE && ( 95 | this.chatManager.chat().info.name === 'localhost' || 96 | this.target().name !== this.chatManager.chat().info.name || 97 | (this.target().name === this.chatManager.chat().info.name && !this.chatManager.isLastMessageVisible()) 98 | ) 99 | ) { 100 | this.stats.messages.new++; 101 | } 102 | this.timestamp = Date.now(); 103 | 104 | // TODO: implement this via EventEmitter 105 | // scroll down to last visible message 106 | if (this.info === this.manager().chat().info) { 107 | this.manager().scrollToLast(); 108 | } 109 | 110 | return message; 111 | } 112 | } 113 | 114 | export class ChatStats { 115 | users: {}; 116 | messages = { 117 | new: 0 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {A11yModule} from '@angular/cdk/a11y'; 3 | import {ClipboardModule} from '@angular/cdk/clipboard'; 4 | import {DragDropModule} from '@angular/cdk/drag-drop'; 5 | import {PortalModule} from '@angular/cdk/portal'; 6 | import {ScrollingModule} from '@angular/cdk/scrolling'; 7 | import {CdkStepperModule} from '@angular/cdk/stepper'; 8 | import {CdkTableModule} from '@angular/cdk/table'; 9 | import {CdkTreeModule} from '@angular/cdk/tree'; 10 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 11 | import {MatBadgeModule} from '@angular/material/badge'; 12 | import {MatBottomSheetModule} from '@angular/material/bottom-sheet'; 13 | import {MatButtonModule} from '@angular/material/button'; 14 | import {MatButtonToggleModule} from '@angular/material/button-toggle'; 15 | import {MatCardModule} from '@angular/material/card'; 16 | import {MatCheckboxModule} from '@angular/material/checkbox'; 17 | import {MatChipsModule} from '@angular/material/chips'; 18 | import {MatStepperModule} from '@angular/material/stepper'; 19 | import {MatDatepickerModule} from '@angular/material/datepicker'; 20 | import {MatDialogModule} from '@angular/material/dialog'; 21 | import {MatDividerModule} from '@angular/material/divider'; 22 | import {MatExpansionModule} from '@angular/material/expansion'; 23 | import {MatGridListModule} from '@angular/material/grid-list'; 24 | import {MatIconModule} from '@angular/material/icon'; 25 | import {MatInputModule} from '@angular/material/input'; 26 | import {MatListModule} from '@angular/material/list'; 27 | import {MatMenuModule} from '@angular/material/menu'; 28 | import {MatNativeDateModule, MatRippleModule} from '@angular/material/core'; 29 | import {MatPaginatorModule} from '@angular/material/paginator'; 30 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 31 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 32 | import {MatRadioModule} from '@angular/material/radio'; 33 | import {MatSelectModule} from '@angular/material/select'; 34 | import {MatSidenavModule} from '@angular/material/sidenav'; 35 | import {MatSliderModule} from '@angular/material/slider'; 36 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 37 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 38 | import {MatSortModule} from '@angular/material/sort'; 39 | import {MatTableModule} from '@angular/material/table'; 40 | import {MatTabsModule} from '@angular/material/tabs'; 41 | import {MatToolbarModule} from '@angular/material/toolbar'; 42 | import {MatTooltipModule} from '@angular/material/tooltip'; 43 | import {MatTreeModule} from '@angular/material/tree'; 44 | 45 | @NgModule({ 46 | exports: [ 47 | A11yModule, 48 | ClipboardModule, 49 | CdkStepperModule, 50 | CdkTableModule, 51 | CdkTreeModule, 52 | DragDropModule, 53 | MatAutocompleteModule, 54 | MatBadgeModule, 55 | MatBottomSheetModule, 56 | MatButtonModule, 57 | MatButtonToggleModule, 58 | MatCardModule, 59 | MatCheckboxModule, 60 | MatChipsModule, 61 | MatStepperModule, 62 | MatDatepickerModule, 63 | MatDialogModule, 64 | MatDividerModule, 65 | MatExpansionModule, 66 | MatGridListModule, 67 | MatIconModule, 68 | MatInputModule, 69 | MatListModule, 70 | MatMenuModule, 71 | MatNativeDateModule, 72 | MatPaginatorModule, 73 | MatProgressBarModule, 74 | MatProgressSpinnerModule, 75 | MatRadioModule, 76 | MatRippleModule, 77 | MatSelectModule, 78 | MatSidenavModule, 79 | MatSliderModule, 80 | MatSlideToggleModule, 81 | MatSnackBarModule, 82 | MatSortModule, 83 | MatTableModule, 84 | MatTabsModule, 85 | MatToolbarModule, 86 | MatTooltipModule, 87 | MatTreeModule, 88 | PortalModule, 89 | ScrollingModule, 90 | ] 91 | }) 92 | export class MaterialModule { 93 | } 94 | 95 | /** 96 | * Copyright 2020 Google LLC. All Rights Reserved. 97 | * Use of this source code is governed by an MIT-style license that 98 | * can be found in the LICENSE file at http://angular.io/license 99 | */ 100 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "themes/material-theme"; 2 | 3 | // other imports 4 | @import '~@ctrl/ngx-emoji-mart/picker'; 5 | 6 | html, body { 7 | height: 100%; 8 | overscroll-behavior-y: contain; 9 | } 10 | body { 11 | margin: 0; 12 | font-family: 'Hind Madurai', Roboto, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | snack-bar-container { 16 | margin-top: 72px !important; 17 | margin-bottom: 72px !important; 18 | } 19 | 20 | a { 21 | color: $accent; 22 | text-decoration: none; 23 | } 24 | 25 | .emoji-dialog-container .mat-dialog-container { 26 | padding: 0; 27 | } 28 | 29 | *:focus { 30 | outline: none; 31 | } 32 | 33 | .mention-container { 34 | z-index: 100; 35 | position: absolute; 36 | left: 50%; 37 | right: 50%; 38 | bottom: 292px; 39 | margin-left: -110px; 40 | width: 0; 41 | height: 0; 42 | .tribute-container { 43 | @extend .mat-app-background; 44 | border-radius: 12px; 45 | box-shadow: 1px 1px 6px 2px rgba(220, 220, 200, 0.5); 46 | overflow: hidden; 47 | overflow-y: scroll; 48 | width: 220px; 49 | height: 160px; 50 | padding-top: 16px; 51 | padding-bottom: 16px; 52 | } 53 | ul { 54 | padding: 0; 55 | margin: 0; 56 | list-style: none; 57 | li { 58 | padding-left: 16px; 59 | padding-right: 16px; 60 | white-space: nowrap; 61 | overflow: hidden; 62 | } 63 | li.highlight { 64 | @extend .theme-background-accent; 65 | } 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | /* IRC COLORS */ 73 | /* 74 | 0 White (255,255,255) 75 | 1 Black (0,0,0) 76 | 2 Blue (0,0,127) 77 | 3 Green (0,147,0) 78 | 4 Light Red (255,0,0) 79 | 5 Brown (127,0,0) 80 | 6 Purple (156,0,156) 81 | 7 Orange (252,127,0) 82 | 8 Yellow (255,255,0) 83 | 9 Light Green (0,252,0) 84 | 10 Cyan (0,147,147) 85 | 11 Light Cyan (0,255,255) 86 | 12 Light Blue (0,0,252) 87 | 13 Pink (255,0,255) 88 | 14 Grey (127,127,127) 89 | 15 Light Grey (210,210,210) 90 | */ 91 | 92 | .ircf-fg-0 { 93 | color: rgb(255,255,255) !important; 94 | } 95 | .ircf-fg-1 { 96 | color: rgb(0,0,0) !important; 97 | } 98 | .ircf-fg-2 { 99 | color: rgb(0,0,127) !important; 100 | } 101 | .ircf-fg-3 { 102 | color: rgb(0,147,0) !important; 103 | } 104 | .ircf-fg-4 { 105 | color: rgb(255,0,0) !important; 106 | } 107 | .ircf-fg-5 { 108 | color: rgb(127,0,0) !important; 109 | } 110 | .ircf-fg-6 { 111 | color: rgb(156,0,156) !important; 112 | } 113 | .ircf-fg-7 { 114 | color: rgb(252,127,0) !important; 115 | } 116 | .ircf-fg-8 { 117 | color: rgb(255,255,0) !important; 118 | } 119 | .ircf-fg-9 { 120 | color: rgb(0,252,0) !important; 121 | } 122 | .ircf-fg-10 { 123 | color: rgb(0,147,147) !important; 124 | } 125 | .ircf-fg-11 { 126 | color: rgb(0,255,255) !important; 127 | } 128 | .ircf-fg-12 { 129 | color: rgb(0,0,252) !important; 130 | } 131 | .ircf-fg-13 { 132 | color: rgb(255,0,255) !important; 133 | } 134 | .ircf-fg-14 { 135 | color: rgb(127,127,127) !important; 136 | } 137 | .ircf-fg-15 { 138 | color: rgb(210,120,120) !important; 139 | } 140 | 141 | .ircf-bg-0 { 142 | background-color: rgb(255,255,255) !important; 143 | } 144 | .ircf-bg-1 { 145 | background-color: rgb(0,0,0) !important; 146 | } 147 | .ircf-bg-2 { 148 | background-color: rgb(0,0,127) !important; 149 | } 150 | .ircf-bg-3 { 151 | background-color: rgb(0,147,0) !important; 152 | } 153 | .ircf-bg-4 { 154 | background-color: rgb(255,0,0) !important; 155 | } 156 | .ircf-bg-5 { 157 | background-color: rgb(127,0,0) !important; 158 | } 159 | .ircf-bg-6 { 160 | background-color: rgb(156,0,156) !important; 161 | } 162 | .ircf-bg-7 { 163 | background-color: rgb(252,127,0) !important; 164 | } 165 | .ircf-bg-8 { 166 | background-color: rgb(255,255,0) !important; 167 | } 168 | .ircf-bg-9 { 169 | background-color: rgb(0,252,0) !important; 170 | } 171 | .ircf-bg-10 { 172 | background-color: rgb(0,147,147) !important; 173 | } 174 | .ircf-bg-11 { 175 | background-color: rgb(0,255,255) !important; 176 | } 177 | .ircf-bg-12 { 178 | background-color: rgb(0,0,252) !important; 179 | } 180 | .ircf-bg-13 { 181 | background-color: rgb(255,0,255) !important; 182 | } 183 | .ircf-bg-14 { 184 | background-color: rgb(127,127,127) !important; 185 | } 186 | .ircf-bg-15 { 187 | background-color: rgb(210,120,120) !important; 188 | } 189 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 26 | import 'classlist.js'; // Run `npm install --save classlist.js`. 27 | 28 | /** 29 | * Web Animations `@angular/platform-browser/animations` 30 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 31 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 32 | */ 33 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 34 | 35 | /** 36 | * By default, zone.js will patch all possible macroTask and DomEvents 37 | * chatUser can disable parts of macroTask/DomEvents patch by setting following flags 38 | * because those flags need to be set before `zone.js` being loaded, and webpack 39 | * will put import in the top of bundle, so chatUser need to create a separate file 40 | * in this directory (for example: zone-flags.ts), and put the following flags 41 | * into that file, and then add the following code before importing zone.js. 42 | * import './zone-flags.ts'; 43 | * 44 | * The flags allowed in zone-flags.ts are listed here. 45 | * 46 | * The following flags will work for all browsers. 47 | * 48 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 49 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 50 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 51 | * 52 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 53 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 54 | * 55 | * (window as any).__Zone_enable_cross_context_check = true; 56 | * 57 | */ 58 | 59 | /*************************************************************************************************** 60 | * Zone JS is required by default for Angular itself. 61 | */ 62 | import 'zone.js/dist/zone'; // Included with Angular CLI. 63 | 64 | 65 | /*************************************************************************************************** 66 | * APPLICATION IMPORTS 67 | */ 68 | import 'fetch-polyfill'; // npm i fetch-polyfill --save 69 | 70 | /* IE9, IE10 and IE11 requires all of the following polyfills. */ 71 | // import 'core-js/es6/symbol'; 72 | // import 'core-js/es6/object'; 73 | // import 'core-js/es6/function'; 74 | // import 'core-js/es6/parse-int'; 75 | // import 'core-js/es6/parse-float'; 76 | // import 'core-js/es6/number'; 77 | // import 'core-js/es6/math'; 78 | // import 'core-js/es6/string'; 79 | // import 'core-js/es6/date'; 80 | // import 'core-js/es6/array'; 81 | // import 'core-js/es6/regexp'; 82 | // import 'core-js/es6/map'; 83 | // import 'core-js/es6/weak-map'; 84 | // import 'core-js/es6/set'; 85 | /* Evergreen browsers require these. */ 86 | // import 'core-js/es6/reflect'; 87 | // import 'core-js/es7/reflect'; 88 | 89 | /** 90 | * Date, currency, decimal and percent pipes. 91 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 92 | */ 93 | import 'intl'; // Run `npm install --save intl`. 94 | /** 95 | * Need to import at least one locale-data with intl. 96 | */ 97 | import 'intl/locale-data/jsonp/en'; 98 | -------------------------------------------------------------------------------- /src/app/chat/pipes/enrich-message.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {Observable, Observer, zip} from 'rxjs'; 4 | import {DomSanitizer} from '@angular/platform-browser'; 5 | /* 6 | export interface MediaInfo { 7 | // 8 | // SEE: https://noembed.com for further info 9 | // 10 | html: string; 11 | // `\n\n`; 13 | version: string; // '1.0'; 14 | height: number; // 270; 15 | thumbnail_url: string; // 'https://i.ytimg.com/vi/srkdnRhnHPM/hqdefault.jpg'; 16 | thumbnail_width: number; // 480; 17 | url: string; // 'https://www.youtube.com/watch?v=srkdnRhnHPM'; 18 | author_name: string; // 'TheChudovische'; 19 | title: string; // 'Amy Winehouse - Live at Porchester Hall [2007]'; 20 | width: number; // 480; 21 | provider_name: string; // 'YouTube'; 22 | provider_url: string; // 'https://www.youtube.com/'; 23 | thumbnail_height: number; // 360; 24 | type: string; // 'video'; 25 | author_url: string; // 'https://www.youtube.com/user/TheChudovische'; 26 | originalUrl: string; // '[https://www.youtube.com/watch?v=srkdnRhnHPM]'; 27 | } 28 | */ 29 | 30 | export interface MediaInfo { 31 | // 32 | // SEE: https://noembed.com for further info 33 | // 34 | html: string; // HTML code for embedding the media (eg. '') 35 | version: string; // '1.0'; 36 | height: number; // 270; 37 | thumbnail_url: string; // 'https://i.ytimg.com/vi/srkdnRhnHPM/hqdefault.jpg'; 38 | thumbnail_width: number; // 480; 39 | url: string; // 'https://www.youtube.com/watch?v=srkdnRhnHPM'; 40 | author_name: string; // 'TheChudovische'; 41 | title: string; // 'Amy Winehouse - Live at Porchester Hall [2007]'; 42 | width: number; // 480; 43 | provider_name: string; // 'YouTube'; 44 | provider_url: string; // 'https://www.youtube.com/'; 45 | thumbnail_height: number; // 360; 46 | type: string; // 'video'; 47 | author_url: string; // 'https://www.youtube.com/user/TheChudovische'; 48 | originalUrl: string; // '[https://www.youtube.com/watch?v=srkdnRhnHPM]'; 49 | } 50 | 51 | @Pipe({ 52 | name: 'enrichMessage' 53 | }) 54 | export class EnrichMessage implements PipeTransform { 55 | static mediaUrlsCache: MediaInfo[] = []; 56 | 57 | enrich(value, httpClient: HttpClient): Observable { 58 | const text = this.createTextLinks_(value); 59 | return new Observable((observer: Observer) => { 60 | observer.next(text.replaced); 61 | const pending: Observable[] = []; 62 | text.urls.forEach((url) => { 63 | const cached = EnrichMessage.mediaUrlsCache.find((v) => v.originalUrl === url || v.url === url); 64 | if (cached != null) { 65 | observer.next(text.replaced.replace(`[${url}]`, `${cached.title} (${cached.provider_name})`)); 66 | return; 67 | } 68 | const o = httpClient.get(`https://noembed.com/embed?url=${url}`); 69 | pending.push(o); 70 | o.subscribe((res: MediaInfo) => { 71 | if (res && res.title) { 72 | res.originalUrl = url; 73 | EnrichMessage.mediaUrlsCache.push(res); 74 | observer.next(text.replaced.replace(`[${url}]`, `${res.title} (${res.provider_name})`)); 75 | console.log(res); 76 | } else { 77 | observer.next(text.replaced.replace(`[${url}]`, url)); 78 | } 79 | }, (err) => { 80 | // TODO: ... 81 | observer.next(text.replaced.replace(`[${url}]`, url)); 82 | }); 83 | }); 84 | zip(...pending).subscribe((res) => { 85 | observer.complete(); 86 | }); 87 | }); 88 | } 89 | 90 | createTextLinks_(text: string) { 91 | const urls: string[] = []; 92 | const replaced = (text || '').replace( 93 | /([^\S]|^)(((https?\:\/\/)|(www\.))(\S+))/gi, 94 | (match, space, url) => { 95 | let hyperlink = url; 96 | if (!hyperlink.match('^https?:\/\/')) { 97 | hyperlink = 'http://' + hyperlink; 98 | } 99 | urls.push(url); 100 | const mediaUrl = '[' + url + ']'; 101 | this.sanitizer.bypassSecurityTrustHtml(mediaUrl); 102 | return space + mediaUrl; 103 | } 104 | ); 105 | return { replaced, urls }; 106 | } 107 | 108 | constructor(private sanitizer: DomSanitizer, private httpClient: HttpClient) {} 109 | 110 | transform(value: string, ...args: any[]): any { 111 | this.enrich(value, this.httpClient); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { LOCALE_ID } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { NgModule } from '@angular/core'; 5 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | import { FlexLayoutModule } from '@angular/flex-layout'; 9 | 10 | import { PickerModule } from '@ctrl/ngx-emoji-mart'; 11 | import { ScrollEventModule } from 'ngx-scroll-event'; 12 | 13 | import { AppComponent } from './app.component'; 14 | import { MessagesWindowComponent } from './chat/messages-window/messages-window.component'; 15 | import { IrcClientService } from './irc-client-service/irc-client-service'; 16 | import { ChatManagerComponent } from './chat/chat-manager/chat-manager.component'; 17 | import { EnrichMessage } from './chat/pipes/enrich-message.pipe'; 18 | import { SortByPipe } from './chat/pipes/sort-by.pipe'; 19 | import { YoutubeVideoComponent } from './socialmedia/youtube-video/youtube-video.component'; 20 | import { SafePipe } from './chat/pipes/safe.pipe'; 21 | import { EmojiDialogComponent } from './chat/dialogs/emoji-dialog/emoji-dialog.component'; 22 | import { SplashScreenComponent } from './splash-screen/splash-screen.component'; 23 | 24 | import { environment } from '../environments/environment'; 25 | import {DeviceDetectorModule} from 'ngx-device-detector'; 26 | import { MediaPlaylistComponent } from './chat/dialogs/media-playlist/media-playlist.component'; 27 | import { ActionPromptComponent } from './chat/dialogs/action-prompt/action-prompt.component'; 28 | import { AwayPromptComponent } from './chat/dialogs/away-prompt/away-prompt.component'; 29 | import { NicknamePromptComponent } from './chat/dialogs/nickname-prompt/nickname-prompt.component'; 30 | import {CallbackPipe} from './chat/pipes/callback.pipe'; 31 | import {RouterModule} from '@angular/router'; 32 | import { ChannelsListComponent } from './chat/dialogs/channels-list/channels-list.component'; 33 | import { UserInfoDialogComponent } from './chat/dialogs/user-info-dialog/user-info-dialog.component'; 34 | 35 | import {MomentModule} from 'ngx-moment'; 36 | import 'moment/locale/it'; 37 | import {NgxTributeModule} from 'ngx-tribute'; 38 | import {PouchDBService} from './services/pouchdb.service'; 39 | import {EncrDecrService} from './services/encr-decr.service'; 40 | import {SettingsService} from './services/settings.service'; 41 | import { YoutubeSearchComponent } from './chat/dialogs/youtube-search/youtube-search.component'; 42 | import {MaterialModule} from './material.module'; 43 | import {MAT_DIALOG_DEFAULT_OPTIONS} from '@angular/material/dialog'; 44 | import { ServiceWorkerModule } from '@angular/service-worker'; 45 | 46 | @NgModule({ 47 | declarations: [ 48 | AppComponent, 49 | MessagesWindowComponent, 50 | ChatManagerComponent, 51 | EnrichMessage, 52 | SortByPipe, 53 | CallbackPipe, 54 | YoutubeVideoComponent, 55 | SafePipe, 56 | EmojiDialogComponent, 57 | SplashScreenComponent, 58 | MediaPlaylistComponent, 59 | ActionPromptComponent, 60 | AwayPromptComponent, 61 | NicknamePromptComponent, 62 | ChannelsListComponent, 63 | UserInfoDialogComponent, 64 | YoutubeSearchComponent 65 | ], 66 | imports: [ 67 | // angular 68 | BrowserModule, 69 | AppRoutingModule, 70 | HttpClientModule, 71 | BrowserAnimationsModule, 72 | FlexLayoutModule, 73 | FormsModule, 74 | ReactiveFormsModule, 75 | // moment.js 76 | MomentModule, 77 | // Material UI 78 | MaterialModule, 79 | // third party 80 | NgxTributeModule, 81 | ScrollEventModule, 82 | PickerModule, 83 | RouterModule.forRoot([], {useHash: true}), 84 | DeviceDetectorModule.forRoot(), 85 | ServiceWorkerModule.register('ngsw-worker.js', { 86 | enabled: environment.production, 87 | // Register the ServiceWorker as soon as the app is stable 88 | // or after 30 seconds (whichever comes first). 89 | registrationStrategy: 'registerWhenStable:30000' 90 | }) 91 | ], 92 | entryComponents: [ 93 | EmojiDialogComponent, 94 | MediaPlaylistComponent, 95 | ActionPromptComponent, 96 | AwayPromptComponent, 97 | NicknamePromptComponent, 98 | ChannelsListComponent, 99 | UserInfoDialogComponent, 100 | YoutubeSearchComponent 101 | ], 102 | providers: [ 103 | HttpClientModule, 104 | IrcClientService, 105 | ChatManagerComponent, 106 | {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: true}}, 107 | {provide: LOCALE_ID, useValue: 'it-IT'}, 108 | PouchDBService, 109 | EncrDecrService, 110 | SettingsService 111 | ], 112 | bootstrap: [ AppComponent ] 113 | }) 114 | export class AppModule {} 115 | -------------------------------------------------------------------------------- /src/app/chat/messages-window/messages-window.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
8 | 9 | 10 | 11 |
12 | 13 |
15 | 16 | 17 | 18 | 19 |
21 | 22 | 25 | 26 | announcement 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 |
39 | 40 | 43 | {{msg.rendered.flagsIconName}} 45 | {{msg.rendered.musicIcon}} 46 | 47 | 48 |
{{msg.timestamp | date: 'mediumTime' : '' : 'en-US'}}
49 |
50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 |
62 | arrow_downward 63 | new messages 64 | arrow_downward 65 |
66 | 67 |
68 | 69 |
70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 |
79 | 80 | arrow_forward 81 | arrow_back 82 | close 83 | arrow_back 84 | info_outline 85 | 86 | 89 | 90 | 91 | {{ boundChat.serviceMessage.message }} 92 | 93 | 94 | 95 |
{{boundChat.serviceMessage.timestamp | date: 'mediumTime' : '' : 'en-US'}}
96 |
97 |
98 | 99 |
100 | 101 |
102 | -------------------------------------------------------------------------------- /src/app/socialmedia/youtube-video/youtube-video.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostListener, OnInit} from '@angular/core'; 2 | 3 | export enum PlayStateEnum { 4 | UNDEFINED = -1, 5 | READY, 6 | PLAYING, 7 | PAUSED, 8 | ENDED 9 | } 10 | 11 | @Component({ 12 | selector: 'app-youtube-video', 13 | templateUrl: './youtube-video.component.html', 14 | styleUrls: ['./youtube-video.component.scss'] 15 | }) 16 | export class YoutubeVideoComponent implements OnInit { 17 | playState: PlayStateEnum = PlayStateEnum.UNDEFINED; 18 | PlayState = PlayStateEnum; 19 | isFullScreen = false; 20 | 21 | private YT: any; 22 | private player: any; 23 | private reframed = false; 24 | private iframe; 25 | private maxStartVolume = 20; 26 | 27 | public videoId: any; 28 | public isMinimized = true; 29 | public hasMargin = true; 30 | 31 | @HostListener('document:fullscreenchange', ['$event']) 32 | @HostListener('document:webkitfullscreenchange', ['$event']) 33 | @HostListener('document:mozfullscreenchange', ['$event']) 34 | @HostListener('document:MSFullscreenChange', ['$event']) 35 | fullScreenMode(e) { 36 | this.isFullScreen = !this.isFullScreen; 37 | } 38 | 39 | init() { 40 | const tag = document.createElement('script'); 41 | tag.src = 'https://www.youtube.com/iframe_api'; 42 | const firstScriptTag = document.getElementsByTagName('script')[0]; 43 | firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); 44 | } 45 | 46 | constructor() { 47 | } 48 | 49 | ngOnInit() { 50 | this.init(); 51 | 52 | window['onYouTubeIframeAPIReady'] = (e) => { 53 | this.YT = window['YT']; 54 | this.reframed = false; 55 | this.player = new window['YT'].Player('youtube-player', { 56 | // videoId: this.video, 57 | playerVars: { 58 | modestbranding: 1, // no youtube logo 59 | wmode: 'transparent', 60 | autoplay: 1, 61 | controls: 1, 62 | fs: 1, // no full screen 63 | rel: 0, // no similar video list at the end 64 | showinfo: 1, // no video info at start 65 | playsinline: 1, // prevent auto-fullscreen when rotating to landscape mode (fix only iOS) 66 | }, 67 | width: '100%', // 100 // 156 // 240 68 | height: '100%', // 56 // 80 // 135 69 | events: { 70 | onStateChange: this.onPlayerStateChange.bind(this), 71 | onError: this.onPlayerError.bind(this), 72 | onReady: (event: any) => { 73 | this.playState = PlayStateEnum.READY; 74 | this.player = event.target; 75 | if (!this.reframed) { 76 | this.reframed = true; 77 | this.iframe = event.target.a; 78 | // reframe(e.target.a); 79 | } 80 | } 81 | } 82 | }); 83 | }; 84 | } 85 | 86 | onMenuControlPause() { 87 | this.player.pauseVideo(); 88 | } 89 | 90 | onMenuControlPlay() { 91 | this.play(); 92 | } 93 | 94 | onMenuControlFullScreen() { 95 | //this.player.playVideo(); // TODO: won't work on mobile 96 | const iframe = this.iframe; 97 | const requestFullScreen = iframe.requestFullScreen || iframe.mozRequestFullScreen || iframe.webkitRequestFullScreen; 98 | if (requestFullScreen) { 99 | requestFullScreen.bind(iframe)(); 100 | } 101 | } 102 | 103 | onMenuControlMinimize() { 104 | this.isMinimized = true; 105 | } 106 | 107 | onMenuControlExpand() { 108 | this.isMinimized = false; 109 | } 110 | 111 | onMenuControlClose() { 112 | this.closePlayer(); 113 | } 114 | 115 | play() { 116 | this.player.setVolume(this.maxStartVolume); 117 | this.player.playVideo(); 118 | } 119 | 120 | closePlayer() { 121 | this.player.stopVideo(); 122 | this.videoId = null; 123 | } 124 | 125 | onPlayerStateChange(event) { 126 | switch (event.data) { 127 | case window['YT'].PlayerState.PLAYING: 128 | this.playState = PlayStateEnum.PLAYING; 129 | if (this.cleanTime() === 0) { 130 | //console.log('started ' + this.cleanTime()); 131 | } else { 132 | //console.log('playing ' + this.cleanTime()) 133 | } 134 | break; 135 | case window['YT'].PlayerState.PAUSED: 136 | this.playState = PlayStateEnum.PAUSED; 137 | if (this.player.getDuration() - this.player.getCurrentTime() != 0) { 138 | //console.log('paused' + ' @ ' + this.cleanTime()); 139 | } 140 | break; 141 | case window['YT'].PlayerState.ENDED: 142 | this.playState = PlayStateEnum.ENDED; 143 | //console.log('ended '); 144 | this.closePlayer(); 145 | break; 146 | } 147 | } 148 | 149 | //utility 150 | 151 | setRightMargin(hasMargin: boolean) { 152 | this.hasMargin = hasMargin; 153 | } 154 | loadVideo(id: string) { 155 | if (id.indexOf('v=') === -1) { 156 | id = id.substring(id.lastIndexOf('/') + 1); 157 | } else { 158 | id = id.split('v=')[1]; 159 | const ampersandPosition = id.indexOf('&'); 160 | if (ampersandPosition !== -1) { 161 | id = id.substring(0, ampersandPosition); 162 | } 163 | } 164 | this.videoId = id; 165 | this.player.loadVideoById(id); 166 | this.play(); 167 | } 168 | 169 | private cleanTime() { 170 | return Math.round(this.player.getCurrentTime()); 171 | } 172 | 173 | private onPlayerError(event) { 174 | console.log('YOUTUBE PLAYER ERROR!!', event); 175 | switch (event.data) { 176 | case 2: 177 | console.log('' + this.videoId) 178 | break; 179 | case 100: 180 | break; 181 | case 101 || 150: 182 | break; 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |
5 | 6 | 7 | 10 |
11 | {{ chatManager && chatManager.chat().target().name || 'NgWebIRC' }} 12 |
13 |
14 | 21 | 28 | 33 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 |
53 | 54 |
55 | 56 | 60 | 61 |

{{ chatManager.client().config.nick }}

62 | 63 | 67 | 68 | 72 | 73 | Away 76 | 77 |
78 | 79 | 83 | 84 | 88 | 92 | 93 | 94 | 95 | Show join/parts 98 | Dark theme 99 | Enable colors 100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 |
108 | Loading 109 |
110 | -------------------------------------------------------------------------------- /src/app/chat/chat-manager/chat-manager.component.scss: -------------------------------------------------------------------------------- 1 | $input-bar-height: 64px; 2 | 3 | :host { 4 | display: flex; 5 | font-size: 110%; 6 | flex-direction: column; 7 | align-content: stretch; 8 | justify-content: stretch; 9 | width: 100%; 10 | height: calc(100% - #{$input-bar-height}); 11 | } 12 | 13 | .mat-drawer { 14 | transform: none !important; 15 | } 16 | 17 | .hidden { 18 | display: none !important; 19 | } 20 | 21 | cdk-virtual-scroll-viewport button mat-icon.first { 22 | margin-left: 6px; 23 | } 24 | cdk-virtual-scroll-viewport button span { 25 | display: inline-block; 26 | width: calc(162px - 16px); /* 16px is the scrollbar width - should not be considered on mobile */ 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | margin-left: 6px; 31 | margin-right: 6px; 32 | } 33 | 34 | .container { 35 | position: relative; 36 | } 37 | 38 | .cover { 39 | width: 100%; 40 | margin-right: 240px; 41 | .face { 42 | margin: 32px; 43 | width: auto; 44 | height: auto; 45 | font-size: 1000%; 46 | opacity: 0.2; 47 | } 48 | } 49 | 50 | app-messages-window { 51 | width: 100%; 52 | margin-right: 240px; 53 | transition: margin-right .3s ease-in-out; 54 | } 55 | .message-input { 56 | font-family: 'Fira Code', monospace !important; 57 | } 58 | 59 | .input-bar { 60 | position: relative; 61 | display: block; 62 | z-index: 1; 63 | max-width: 100vw; 64 | padding-left: 8px; 65 | padding-right: 8px; 66 | height: $input-bar-height; 67 | margin-right: 240px; 68 | transition: margin-right .3s ease-in-out; 69 | mat-form-field { 70 | margin-top: 8px; 71 | } 72 | button { 73 | margin-bottom: 10px; 74 | } 75 | } 76 | .input-bar.full-width { 77 | margin-right: 0; 78 | } 79 | 80 | .action-buttons { 81 | width: 200px; 82 | min-width: 200px; 83 | max-width: 200px; 84 | } 85 | 86 | .user-menu-title { 87 | /* color: $accent;*/ 88 | font-weight: 700; 89 | font-size: 14px; 90 | padding: 8px; 91 | margin-left: 8px; 92 | margin-right: 8px; 93 | } 94 | 95 | .users-list { 96 | border: 0; 97 | border-left: solid 1px; 98 | font-size: 100%; 99 | z-index: 100; 100 | position: absolute; 101 | transition: margin-right .3s ease-in-out; 102 | right: 0; 103 | opacity: 0.95; 104 | width: 240px; 105 | height: calc(100% + #{$input-bar-height}); 106 | min-width: 240px; 107 | overflow: hidden; 108 | overflow-y: auto; 109 | } 110 | .v-scroll { 111 | height: 100%; 112 | overflow-x: hidden; 113 | overflow-y: auto; 114 | } 115 | .v-scroll button { 116 | padding-left: 12px; 117 | } 118 | 119 | .conversations-list { 120 | border: 0; 121 | border-left: solid 1px; 122 | z-index: 100; 123 | position: absolute; 124 | transition: margin-right .3s ease-in-out; 125 | right: 0; 126 | opacity: 0.95; 127 | width: 240px; 128 | height: calc(100% + #{$input-bar-height}); 129 | min-width: 240px; 130 | overflow: hidden; 131 | overflow-y: auto; 132 | } 133 | 134 | .users-list button.user { 135 | margin-top: 2px; 136 | margin-bottom: 2px; 137 | padding-left: 2px; 138 | padding-right: 2px; 139 | width: 100%; 140 | font-size: 100%; 141 | text-align: left; /* IE / Edge compatibility */ 142 | text-align: start; 143 | .mat-icon { 144 | margin-left: 4px; 145 | margin-right: 4px; 146 | opacity: 0.9; 147 | } 148 | } 149 | .users-list button.is-away { 150 | opacity: 0.4; 151 | } 152 | 153 | .users-list .title, .conversations-list .title { 154 | /* color: $primary;*/ 155 | text-align: center; 156 | width: 100%; 157 | white-space: nowrap; 158 | overflow: hidden; 159 | text-overflow: ellipsis; 160 | height: 42px; 161 | line-height: 42px; 162 | vertical-align: middle; 163 | } 164 | .conversations-list { 165 | .chat-type { 166 | margin-top: 16px; 167 | margin-left: 10px; 168 | /* color: $accent;*/ 169 | font-weight: 600; 170 | font-size: 90%; 171 | text-transform: uppercase; 172 | } 173 | .title { 174 | padding-left: 0; 175 | } 176 | .chat-row { 177 | position: relative; 178 | .notifications-off-badge { 179 | z-index: 10; 180 | position: absolute; 181 | right: 6px; 182 | top: 6px; 183 | width: 24px; 184 | height: 24px; 185 | opacity: 0.35; 186 | } 187 | .chat-button { 188 | margin-top: 16px; 189 | margin-right: 16px; 190 | } 191 | .chat-button-menu { 192 | margin-top: 16px; 193 | } 194 | } 195 | } 196 | .users-list .search-field { 197 | height: 46px; 198 | line-height: 46px; 199 | width: 100%; 200 | border-bottom: 1px solid; 201 | input[type="text"] { 202 | border: 0; 203 | font-size: 100%; 204 | width: 100%; 205 | } 206 | } 207 | 208 | .full-width { 209 | margin-right: 0 !important; 210 | } 211 | .right-panel-show { 212 | margin-right: 0 !important; 213 | } 214 | .right-panel-hide { 215 | margin-right: -240px !important; 216 | } 217 | 218 | .hidden { 219 | display: none; 220 | } 221 | 222 | .statusOverlay { 223 | position: absolute; 224 | top: 0; left: 0; bottom: 0; right: 0; 225 | background-color: rgba(0,0,0,0.8); 226 | padding: 12px; 227 | margin-right: 240px; 228 | margin-bottom: $input-bar-height; 229 | transition: margin-right .3s ease-in-out; 230 | .tool-bar { 231 | width: 100%; 232 | max-width: 420px; 233 | margin-top: 32px; 234 | button { 235 | width: 80px; 236 | } 237 | } 238 | } 239 | 240 | @media only screen and (max-device-width: 640px) { 241 | /* Styles */ 242 | app-messages-window { 243 | margin-right: 0; 244 | } 245 | .users-list { 246 | margin-right: -240px; 247 | } 248 | .conversations-list { 249 | margin-right: -240px; 250 | } 251 | .statusOverlay { 252 | margin-right: 0; 253 | } 254 | } 255 | /* break point for material toolbar (from 64px height to 56px) */ 256 | @media only screen and (max-device-width: 599px) { // small screen 257 | :host { 258 | height: calc(100% - #{$input-bar-height}); 259 | font-size: 85%; 260 | } 261 | .input-bar { 262 | font-size: 140%; 263 | } 264 | } 265 | @media only screen and (max-device-width: 1024px) { 266 | :host { 267 | font-size: 90%; 268 | } 269 | .input-bar { 270 | font-size: 130%; 271 | } 272 | } 273 | @media only screen and (max-device-width: 1280px) { 274 | :host { 275 | font-size: 95%; 276 | } 277 | .input-bar { 278 | font-size: 120%; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng-web-irc": { 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/en/", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": false, 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets", 29 | "src/manifest.webmanifest" 30 | ], 31 | "styles": [ 32 | "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", 33 | "src/styles.scss" 34 | ], 35 | "scripts": [ 36 | "./node_modules/crypto-js/crypto-js.js" 37 | ] 38 | }, 39 | "configurations": { 40 | "production": { 41 | "fileReplacements": [ 42 | { 43 | "replace": "src/environments/environment.ts", 44 | "with": "src/environments/environment.prod.ts" 45 | } 46 | ], 47 | "optimization": true, 48 | "outputHashing": "all", 49 | "sourceMap": false, 50 | "extractCss": true, 51 | "namedChunks": false, 52 | "aot": true, 53 | "extractLicenses": true, 54 | "vendorChunk": false, 55 | "buildOptimizer": true, 56 | "budgets": [ 57 | { 58 | "type": "initial", 59 | "maximumWarning": "2mb", 60 | "maximumError": "5mb" 61 | }, 62 | { 63 | "type": "anyComponentStyle", 64 | "maximumWarning": "6kb", 65 | "maximumError": "10kb" 66 | } 67 | ], 68 | "serviceWorker": true, 69 | "ngswConfigPath": "ngsw-config.json" 70 | }, 71 | "it": { 72 | "aot": true, 73 | "outputPath": "dist/it/", 74 | "i18nFile": "src/locale/messages.it.xlf", 75 | "i18nFormat": "xlf", 76 | "i18nLocale": "it", 77 | "i18nMissingTranslation": "error", 78 | "fileReplacements": [ 79 | { 80 | "replace": "src/environments/environment.ts", 81 | "with": "src/environments/environment.prod.ts" 82 | } 83 | ], 84 | "optimization": true, 85 | "outputHashing": "all", 86 | "sourceMap": false, 87 | "extractCss": true, 88 | "namedChunks": false, 89 | "extractLicenses": true, 90 | "vendorChunk": false, 91 | "buildOptimizer": true, 92 | "budgets": [ 93 | { 94 | "type": "initial", 95 | "maximumWarning": "2mb", 96 | "maximumError": "5mb" 97 | }, 98 | { 99 | "type": "anyComponentStyle", 100 | "maximumWarning": "6kb", 101 | "maximumError": "10kb" 102 | } 103 | ], 104 | "serviceWorker": true, 105 | "ngswConfigPath": "ngsw-config.json" 106 | }, 107 | "en": { 108 | "aot": true, 109 | "outputPath": "dist/en/", 110 | "i18nFile": "src/locale/messages.en.xlf", 111 | "i18nFormat": "xlf", 112 | "i18nLocale": "en", 113 | "i18nMissingTranslation": "error", 114 | "fileReplacements": [ 115 | { 116 | "replace": "src/environments/environment.ts", 117 | "with": "src/environments/environment.prod.ts" 118 | } 119 | ], 120 | "optimization": true, 121 | "outputHashing": "all", 122 | "sourceMap": false, 123 | "extractCss": true, 124 | "namedChunks": false, 125 | "extractLicenses": true, 126 | "vendorChunk": false, 127 | "buildOptimizer": true, 128 | "budgets": [ 129 | { 130 | "type": "initial", 131 | "maximumWarning": "2mb", 132 | "maximumError": "5mb" 133 | }, 134 | { 135 | "type": "anyComponentStyle", 136 | "maximumWarning": "6kb", 137 | "maximumError": "10kb" 138 | } 139 | ], 140 | "serviceWorker": true, 141 | "ngswConfigPath": "ngsw-config.json" 142 | } 143 | } 144 | }, 145 | "serve": { 146 | "builder": "@angular-devkit/build-angular:dev-server", 147 | "options": { 148 | "browserTarget": "ng-web-irc:build" 149 | }, 150 | "configurations": { 151 | "production": { 152 | "browserTarget": "ng-web-irc:build:production" 153 | }, 154 | "it": { 155 | "browserTarget": "ng-web-irc:build:it" 156 | }, 157 | "en": { 158 | "browserTarget": "ng-web-irc:build:en" 159 | } 160 | } 161 | }, 162 | "extract-i18n": { 163 | "builder": "@angular-devkit/build-angular:extract-i18n", 164 | "options": { 165 | "browserTarget": "ng-web-irc:build" 166 | } 167 | }, 168 | "test": { 169 | "builder": "@angular-devkit/build-angular:karma", 170 | "options": { 171 | "main": "src/test.ts", 172 | "polyfills": "src/polyfills.ts", 173 | "tsConfig": "tsconfig.spec.json", 174 | "karmaConfig": "karma.conf.js", 175 | "assets": [ 176 | "src/favicon.ico", 177 | "src/assets", 178 | "src/manifest.webmanifest" 179 | ], 180 | "styles": [ 181 | "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", 182 | "src/styles.scss" 183 | ], 184 | "scripts": [] 185 | } 186 | }, 187 | "lint": { 188 | "builder": "@angular-devkit/build-angular:tslint", 189 | "options": { 190 | "tsConfig": [ 191 | "tsconfig.app.json", 192 | "tsconfig.spec.json", 193 | "e2e/tsconfig.json" 194 | ], 195 | "exclude": [ 196 | "**/node_modules/**" 197 | ] 198 | } 199 | }, 200 | "e2e": { 201 | "builder": "@angular-devkit/build-angular:protractor", 202 | "options": { 203 | "protractorConfig": "e2e/protractor.conf.js", 204 | "devServerTarget": "ng-web-irc:serve" 205 | }, 206 | "configurations": { 207 | "production": { 208 | "devServerTarget": "ng-web-irc:serve:production" 209 | } 210 | } 211 | } 212 | } 213 | } 214 | }, 215 | "defaultProject": "ng-web-irc" 216 | } 217 | -------------------------------------------------------------------------------- /src/locale/messages.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pause 7 | Pause 8 | 9 | 10 | Play 11 | Play 12 | 13 | 14 | Enlarge 15 | Enlarge 16 | 17 | 18 | Shrink 19 | Shrink 20 | 21 | 22 | Close 23 | Close 24 | 25 | 26 | Shared videos 27 | Shared videos 28 | 29 | 30 | ✔ Follow 31 | ✔ Follow 32 | 33 | 34 | Follow 35 | Follow 36 | 37 | 38 | Channels 39 | Channels 40 | 41 | 42 | Enter channel name (e.g. #chat) 43 | Enter channel name (e.g. #chat) 44 | 45 | 46 | List 47 | List 48 | 49 | 50 | Join 51 | Join 52 | 53 | 54 | Search video 55 | Search video 56 | 57 | 58 | Author, title 59 | Author, title 60 | 61 | 62 | Enter at least 3 characters. 63 | Enter at least 3 characters. 64 | 65 | 66 | Join channel 67 | Join channel 68 | 69 | 70 | Your channels 71 | Your channels 72 | 73 | 74 | Private chats 75 | Private chats 76 | 77 | 78 | User name... 79 | User name... 80 | 81 | 82 | Offline 83 | Offline 84 | 85 | 86 | Mention 87 | Mention 88 | 89 | 90 | Chat 91 | Chat 92 | 93 | 94 | Playlist 95 | Playlist 96 | 97 | 98 | Info 99 | Info 100 | 101 | 102 | Find user 103 | Find user 104 | 105 | 106 | Disable notifications 107 | Disable notifications 108 | 109 | 110 | Enable notifications 111 | Enable notifications 112 | 113 | 114 | Hide panel 115 | Hide panel 116 | 117 | 118 | Leave channel 119 | Leave channel 120 | 121 | 122 | Describe action or status 123 | Describe action or status 124 | 125 | 126 | Enter message 127 | Enter message 128 | 129 | 130 | Confirm 131 | Confirm 132 | 133 | 134 | Cancel 135 | Cancel 136 | 137 | 138 | Set away 139 | Set away 140 | 141 | 142 | When set away, users writing to you will receive an automatic message. 143 | When set away, users writing to you will receive an automatic message. 144 | 145 | 146 | Away message text 147 | Away message text 148 | 149 | 150 | User nickname 151 | User nickname 152 | 153 | 154 | Enter nickname 155 | Enter nickname 156 | 157 | 158 | Characters not allowed. 159 | Characters not allowed. 160 | 161 | 162 | Enter at least 3 characters. 163 | Enter at least 3 characters. 164 | 165 | 166 | Registered user 167 | Registered user 168 | 169 | 170 | Password 171 | Password 172 | 173 | 174 | Channel users 175 | Channel users 176 | 177 | 178 | Messages 179 | Messages 180 | 181 | 182 | Change nickname 183 | Change nickname 184 | 185 | 186 | Action 187 | Action 188 | 189 | 190 | Disconnect 191 | Disconnect 192 | 193 | 194 | Connect 195 | Connect 196 | 197 | 198 | Settings 199 | Settings 200 | 201 | 202 | Show join/parts 203 | Show join/parts 204 | 205 | 206 | Dark theme 207 | Dark theme 208 | 209 | 210 | Enable colors 211 | Enable colors 212 | 213 | 214 | Server 215 | Server 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/locale/messages.it.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pause 7 | Pausa 8 | 9 | 10 | Play 11 | Riproduci 12 | 13 | 14 | Enlarge 15 | Ingrandisci 16 | 17 | 18 | Shrink 19 | Riduci 20 | 21 | 22 | Close 23 | Chiudi 24 | 25 | 26 | Shared videos 27 | Video condivisi 28 | 29 | 30 | ✔ Follow 31 | ✔ Segui 32 | 33 | 34 | Follow 35 | Segui 36 | 37 | 38 | Channels 39 | Canali 40 | 41 | 42 | Enter channel name (e.g. #chat) 43 | Inserire nome canale (es. #chat) 44 | 45 | 46 | List 47 | Elenco 48 | 49 | 50 | Join 51 | Entra 52 | 53 | 54 | Search video 55 | Cerca video 56 | 57 | 58 | Author, title 59 | Autore, titolo 60 | 61 | 62 | Enter at least 3 characters. 63 | Inserire almeno 3 caratteri. 64 | 65 | 66 | Join channel 67 | Entra in un canale 68 | 69 | 70 | Your channels 71 | I tuoi canali 72 | 73 | 74 | Private chats 75 | Chat private 76 | 77 | 78 | User name... 79 | Nome utente... 80 | 81 | 82 | Offline 83 | Disconnesso 84 | 85 | 86 | Mention 87 | Menziona 88 | 89 | 90 | Chat 91 | Chat 92 | 93 | 94 | Playlist 95 | Playlist 96 | 97 | 98 | Info 99 | Info 100 | 101 | 102 | Find user 103 | Trova utente 104 | 105 | 106 | Disable notifications 107 | Disabilita notifiche 108 | 109 | 110 | Enable notifications 111 | Abilita notifiche 112 | 113 | 114 | Hide panel 115 | Nascondi pannello 116 | 117 | 118 | Leave channel 119 | Esci dal canale 120 | 121 | 122 | Describe action or status 123 | Descrivi azione o stato 124 | 125 | 126 | Enter message 127 | Inserire messaggio 128 | 129 | 130 | Confirm 131 | Conferma 132 | 133 | 134 | Cancel 135 | Annulla 136 | 137 | 138 | Set away 139 | Imposta assente 140 | 141 | 142 | When set away, users writing to you will receive an automatic message. 143 | Quando assente, gli utenti che ti scrivono riceveranno un messaggio automatico. 144 | 145 | 146 | Away message text 147 | Testo del messaggio 148 | 149 | 150 | User nickname 151 | Nome utente 152 | 153 | 154 | Enter nickname 155 | Inserire nome 156 | 157 | 158 | Characters not allowed. 159 | Caratteri non consentiti. 160 | 161 | 162 | Enter at least 3 characters. 163 | Inserire almeno 3 caratteri. 164 | 165 | 166 | Registered user 167 | Utente registrato 168 | 169 | 170 | Password 171 | Password 172 | 173 | 174 | Channel users 175 | Utenti canale 176 | 177 | 178 | Messages 179 | Messaggi 180 | 181 | 182 | Change nickname 183 | Cambia nome 184 | 185 | 186 | Action 187 | Azione 188 | 189 | 190 | Disconnect 191 | Disconnetti 192 | 193 | 194 | Connect 195 | Connetti 196 | 197 | 198 | Settings 199 | Impostazioni 200 | 201 | 202 | Show join/parts 203 | Mostra entrate/uscite 204 | 205 | 206 | Dark theme 207 | Tema scuro 208 | 209 | 210 | Enable colors 211 | Abilita colori 212 | 213 | 214 | Server 215 | Server 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, HostListener, OnDestroy, OnInit, ViewChild} from '@angular/core'; 2 | 3 | import {ChatManagerComponent} from './chat/chat-manager/chat-manager.component'; 4 | import {LoginInfo} from './irc-client-service/login-info'; 5 | import {ActionPromptComponent} from './chat/dialogs/action-prompt/action-prompt.component'; 6 | import {AwayPromptComponent} from './chat/dialogs/away-prompt/away-prompt.component'; 7 | import {DeviceDetectorService} from 'ngx-device-detector'; 8 | import {NicknamePromptComponent} from './chat/dialogs/nickname-prompt/nickname-prompt.component'; 9 | import {MediaPlaylistComponent} from './chat/dialogs/media-playlist/media-playlist.component'; 10 | import {ChatUser} from './chat/chat-user'; 11 | import {ActivatedRoute, Router} from '@angular/router'; 12 | import {Location} from '@angular/common'; 13 | import {IrcClientService} from './irc-client-service/irc-client-service'; 14 | import {ChannelsListComponent} from './chat/dialogs/channels-list/channels-list.component'; 15 | import {PrivateChat} from './chat/private-chat'; 16 | 17 | import {SettingsService} from './services/settings.service'; 18 | import {MatSidenav} from '@angular/material/sidenav'; 19 | import {MatDialog} from '@angular/material/dialog'; 20 | 21 | @Component({ 22 | selector: 'app-root', 23 | templateUrl: './app.component.html', 24 | styleUrls: ['./app.component.scss'] 25 | }) 26 | export class AppComponent implements OnInit, OnDestroy { 27 | @ViewChild('sidenav', {static: false}) 28 | public sidenav: MatSidenav; 29 | @ViewChild('chatManager', {read: ChatManagerComponent, static: false}) 30 | private channelManager: ChatManagerComponent; 31 | 32 | title = 'ng-web-irc'; 33 | isUserLogged = false; 34 | isUserAway = false; 35 | isLoadingChat = false; 36 | 37 | screenWidth: number; 38 | screenHeight: number; 39 | sideOverBreakPoint = 768; 40 | 41 | mediaPlaylistCount = 0; 42 | mediaPlaylistNotify = false; 43 | 44 | private mediaCountInterval = setInterval(() => { 45 | const channelManager = this.channelManager; 46 | if (channelManager && channelManager.currentChat) { 47 | if (channelManager.isPublicChat(channelManager.currentChat.info)) { 48 | const count = channelManager 49 | .channel().users 50 | .reduce((a: number, b: ChatUser) => a + b.playlist.length, 0); 51 | if (count > this.mediaPlaylistCount) { 52 | this.mediaPlaylistNotify = true; 53 | } 54 | this.mediaPlaylistCount = count; 55 | } else { 56 | const count = (channelManager.currentChat as PrivateChat).user.playlist.length; 57 | if (count > this.mediaPlaylistCount) { 58 | this.mediaPlaylistNotify = true; 59 | } 60 | this.mediaPlaylistCount = count; 61 | } 62 | } else { 63 | this.mediaPlaylistCount = 0; 64 | } 65 | }, 2000); 66 | 67 | constructor( 68 | public dialog: MatDialog, 69 | public deviceService: DeviceDetectorService, 70 | public ircClientService: IrcClientService, 71 | public settingsService: SettingsService, 72 | private router: Router, 73 | private route: ActivatedRoute, 74 | private locationService: Location 75 | ) { } 76 | 77 | @HostListener('window:resize', ['$event']) 78 | onResize(event?) { 79 | this.screenHeight = window.innerHeight; 80 | this.screenWidth = window.innerWidth; 81 | } 82 | 83 | ngOnInit(): void { 84 | this.screenHeight = window.innerHeight; 85 | this.screenWidth = window.innerWidth; 86 | this.settingsService.loadSettings(); 87 | // handle nick name errors 88 | this.ircClientService.invalidNick.subscribe((msg) => { 89 | this.onNickChangeClick(this.channelManager); 90 | }); 91 | } 92 | ngOnDestroy() { 93 | if (this.mediaCountInterval) { 94 | clearInterval(this.mediaCountInterval); 95 | } 96 | } 97 | 98 | onConnectRequest(credentials: LoginInfo) { 99 | this.ircClientService.setCredentials(credentials); 100 | this.ircClientService.saveConfiguration(); 101 | this.isUserLogged = true; 102 | } 103 | onChannelUsersButtonClick(chatManager: ChatManagerComponent) { 104 | chatManager.showChatUsers(); 105 | } 106 | onChatMessagesButtonClick(chatManager: ChatManagerComponent) { 107 | chatManager.showChatList(); 108 | } 109 | onChannelPlaylistButtonClick(chatManager: ChatManagerComponent) { 110 | if (this.screenWidth < 640) { 111 | this.channelManager.closeRightPanel(); 112 | } 113 | this.mediaPlaylistNotify = false; 114 | this.router.navigate(['.'], { fragment: 'playlist', relativeTo: this.route }); 115 | const dialogRef = this.dialog.open(MediaPlaylistComponent, { 116 | width: '330px', 117 | data: chatManager.currentChat == null ? [] : 118 | (chatManager.isPublicChat(chatManager.currentChat.info) 119 | ? chatManager.channel().users 120 | : [(chatManager.currentChat as PrivateChat).user]), 121 | closeOnNavigation: true 122 | }); 123 | dialogRef.componentInstance.followingUser = this.channelManager.followingUserPlaylist; 124 | dialogRef.componentInstance.following.subscribe((u) => { 125 | this.channelManager.followingUserPlaylist = u; 126 | }); 127 | dialogRef.afterClosed().subscribe(media => { 128 | if (this.router.url.indexOf('#playlist') > 0) { 129 | this.locationService.back(); 130 | } 131 | if (media) { 132 | let videoId = ''; 133 | if (media.url.indexOf('v=') === -1) { 134 | videoId = media.url.substring(media.url.lastIndexOf('/') + 1); 135 | } else { 136 | videoId = media.url.split('v=')[1]; 137 | const ampersandPosition = videoId.indexOf('&'); 138 | if (ampersandPosition !== -1) { 139 | videoId = videoId.substring(0, ampersandPosition); 140 | } 141 | } 142 | this.channelManager.videoPlayer.loadVideo(videoId); 143 | } 144 | }); 145 | } 146 | 147 | onUserActionClick(chatManager: ChatManagerComponent) { 148 | if (this.deviceService.isMobile()) { 149 | this.sidenav.close(); 150 | } 151 | this.router.navigate(['.'], { fragment: 'action', relativeTo: this.route }); 152 | const dialogRef = this.dialog.open(ActionPromptComponent, { 153 | width: '330px', 154 | data: chatManager.client().nick(), 155 | closeOnNavigation: true 156 | }); 157 | dialogRef.afterClosed().subscribe(res => { 158 | if (res && res.length > 0) { 159 | chatManager.userAction(res); 160 | } 161 | }); 162 | } 163 | onSetAwayClick(e, chatManager: ChatManagerComponent) { 164 | if (this.deviceService.isMobile()) { 165 | this.sidenav.close(); 166 | } 167 | if (this.isUserAway) { 168 | this.router.navigate(['.'], { relativeTo: this.route, replaceUrl: true }); 169 | chatManager.setAway(''); 170 | e.source.checked = this.isUserAway = false; 171 | } else { 172 | this.router.navigate(['.'], { fragment: 'action', relativeTo: this.route }); 173 | const dialogRef = this.dialog.open(AwayPromptComponent, { 174 | width: '330px', 175 | closeOnNavigation: true 176 | }); 177 | dialogRef.afterClosed().subscribe(res => { 178 | if (res !== chatManager.awayMessage) { 179 | chatManager.setAway(res); 180 | this.isUserAway = (chatManager.awayMessage.length > 0); 181 | } 182 | e.source.checked = this.isUserAway; 183 | }); 184 | } 185 | } 186 | onNickChangeClick(chatManager: ChatManagerComponent) { 187 | if (this.deviceService.isMobile()) { 188 | this.sidenav.close(); 189 | } 190 | this.router.navigate(['.'], { fragment: 'action', relativeTo: this.route }); 191 | const dialogRef = this.dialog.open(NicknamePromptComponent, { 192 | data: { 193 | nick: chatManager.client().nick(), 194 | password: chatManager.client().config.password 195 | }, 196 | closeOnNavigation: true 197 | }); 198 | dialogRef.afterClosed().subscribe(res => { 199 | if (res) { 200 | chatManager.client().nick(res.nick); 201 | if (res.password && res.password.length > 0) { 202 | chatManager.client().config.password = res.password; 203 | chatManager.client().identify(); 204 | } 205 | } 206 | }); 207 | } 208 | onChannelListClick(chatManager: ChatManagerComponent) { 209 | if (this.deviceService.isMobile()) { 210 | this.sidenav.close(); 211 | } 212 | this.router.navigate(['.'], { fragment: 'channels', relativeTo: this.route }); 213 | const dialogRef = this.dialog.open(ChannelsListComponent, { 214 | width: '330px', 215 | closeOnNavigation: true 216 | }); 217 | dialogRef.afterClosed().subscribe(res => { 218 | if (res && res.length > 0) { 219 | this.ircClientService.join(res); 220 | chatManager.show(res); 221 | } 222 | }); 223 | } 224 | onShowJoinPartsChange(e, chatManager: ChatManagerComponent) { 225 | if (this.deviceService.isMobile()) { 226 | this.sidenav.close(); 227 | } 228 | chatManager.channel().preferences.showChannelActivityToggle(); 229 | } 230 | onDisconnectClick(chatManager: ChatManagerComponent) { 231 | if (this.deviceService.isMobile()) { 232 | this.sidenav.close(); 233 | } 234 | chatManager.disconnect(); 235 | } 236 | onConnectClick(chatManager: ChatManagerComponent) { 237 | if (this.deviceService.isMobile()) { 238 | this.sidenav.close(); 239 | } 240 | chatManager.connect(); 241 | } 242 | onChatLoading(loading) { 243 | this.isLoadingChat = loading; 244 | } 245 | onToggleDarkTheme(checked: boolean) { 246 | if (this.deviceService.isMobile()) { 247 | this.sidenav.close(); 248 | } 249 | this.settingsService.settings.isDarkTheme = checked; 250 | this.settingsService.saveSettings(); 251 | } 252 | onToggleShowColors(checked: boolean) { 253 | if (this.deviceService.isMobile()) { 254 | this.sidenav.close(); 255 | } 256 | this.settingsService.settings.showColors = checked; 257 | this.settingsService.saveSettings(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/app/chat/chat-manager/chat-manager.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 5 | 6 |
9 | sentiment_satisfied_alt 10 | 11 |
14 | 15 | 22 | 27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 |
38 | 39 |
40 | Your channels ({{ boundChatList.activePublicChats() }}) 41 |
42 |
43 | 46 | 54 | notifications_off 55 |
56 |
57 | Private chats ({{ boundChatList.activePrivateChats() }}) 58 |
59 |
60 | 64 | 72 | notifications_off 73 |
74 |
75 | 76 |
80 |
81 | 85 |
86 | 91 | 96 |
97 |
98 | {{ currentChat.info.name }} 99 |
100 |
101 | 103 | 112 | 113 |
114 | 115 | 116 |
{{currentUser.user.name}}
117 | 118 | 122 | 123 | 128 | 129 | 134 | 135 | 143 | 144 | 148 |
149 | 150 | 151 | 152 |
{{ currentChat.info.name }}
153 | 157 | 158 | 164 | 165 | 169 | 170 | 174 |
175 |
176 | 177 | 178 | 179 |
{{ currentMenuChat.info.name }}
180 | 185 | 186 | 192 | 193 | 197 | 198 | 202 |
203 |
204 | 205 |
206 | 207 |
210 | 211 |

{{currentChat.info.name}}

212 | 213 |
214 | This channel is invite only. 215 |
216 |
217 | Only registered users can join this channel. 218 |
219 |
220 | You've been kicked from this channel. 221 |
222 |
223 | You are banned from this channel. 224 |
225 | 226 |
227 | 231 | 235 |
236 | 237 |
238 | 239 |
241 | 242 |
243 | 244 | 245 | 255 | 256 | 259 | 262 | 265 | 266 |
267 | 268 |
269 | 270 |
271 | --------------------------------------------------------------------------------