Compare commits
No commits in common. "e46c71654767ecdf24f107ab80623e9847633d9c" and "a393bd91aba2ed9c576527e775cd41612012e553" have entirely different histories.
e46c716547
...
a393bd91ab
@ -1,42 +0,0 @@
|
|||||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
|
||||||
|
|
||||||
# Compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
/out-tsc
|
|
||||||
/bazel-out
|
|
||||||
|
|
||||||
# Node
|
|
||||||
/node_modules
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
.idea/
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# Visual Studio Code
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.history/*
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
/.angular/cache
|
|
||||||
.sass-cache/
|
|
||||||
/connect.lock
|
|
||||||
/coverage
|
|
||||||
/libpeerconnection.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# System files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
22
Dockerfile
@ -1,22 +0,0 @@
|
|||||||
FROM node:23-alpine AS build
|
|
||||||
|
|
||||||
ARG config production
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY package*.json .
|
|
||||||
|
|
||||||
RUN npm install -g @angular/cli
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY . /src
|
|
||||||
RUN ng build \
|
|
||||||
--configuration=${config} \
|
|
||||||
--delete-output-path false
|
|
||||||
|
|
||||||
FROM nginx:alpine-slim
|
|
||||||
|
|
||||||
COPY --from=build /src/dist/ui/browser/ /usr/share/nginx/html
|
|
||||||
|
|
||||||
COPY nginx/default.conf /etc/nginx/conf.d/
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
47
angular.json
@ -15,7 +15,7 @@
|
|||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/ui",
|
"outputPath": "dist/ui",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
@ -33,14 +33,9 @@
|
|||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/ngx-toastr/toastr.css",
|
"node_modules/ngx-toastr/toastr.css",
|
||||||
"node_modules/prismjs/themes/prism-okaidia.css",
|
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": [
|
"scripts": []
|
||||||
"node_modules/prismjs/prism.js",
|
|
||||||
"node_modules/prismjs/components/prism-csharp.min.js",
|
|
||||||
"node_modules/prismjs/components/prism-css.min.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -62,8 +57,7 @@
|
|||||||
"maximumError": "8kB"
|
"maximumError": "8kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all",
|
"outputHashing": "all"
|
||||||
"serviceWorker": "ngsw-config.json"
|
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
@ -74,7 +68,7 @@
|
|||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "ui:build:production"
|
"buildTarget": "ui:build:production"
|
||||||
@ -87,10 +81,10 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular/build:extract-i18n"
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
@ -112,34 +106,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"type": "component"
|
|
||||||
},
|
|
||||||
"@schematics/angular:directive": {
|
|
||||||
"type": "directive"
|
|
||||||
},
|
|
||||||
"@schematics/angular:service": {
|
|
||||||
"type": "service"
|
|
||||||
},
|
|
||||||
"@schematics/angular:guard": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:interceptor": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:module": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:pipe": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
},
|
|
||||||
"@schematics/angular:resolver": {
|
|
||||||
"typeSeparator": "."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"analytics": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
|
||||||
"index": "/index.html",
|
|
||||||
"assetGroups": [
|
|
||||||
{
|
|
||||||
"name": "app",
|
|
||||||
"installMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/favicon.ico",
|
|
||||||
"/index.csr.html",
|
|
||||||
"/index.html",
|
|
||||||
"/manifest.webmanifest",
|
|
||||||
"/*.css",
|
|
||||||
"/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "assets",
|
|
||||||
"installMode": "lazy",
|
|
||||||
"updateMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
8655
package-lock.json
generated
31
package.json
@ -10,28 +10,25 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.0.2",
|
"@angular/animations": "^19.2.14",
|
||||||
"@angular/cdk": "^20.0.2",
|
"@angular/cdk": "^19.2.18",
|
||||||
"@angular/common": "^20.0.2",
|
"@angular/common": "^19.2.0",
|
||||||
"@angular/compiler": "^20.0.2",
|
"@angular/compiler": "^19.2.0",
|
||||||
"@angular/core": "^20.0.2",
|
"@angular/core": "^19.2.0",
|
||||||
"@angular/forms": "^20.0.2",
|
"@angular/forms": "^19.2.0",
|
||||||
"@angular/material": "^20.0.2",
|
"@angular/material": "^19.2.18",
|
||||||
"@angular/platform-browser": "^20.0.2",
|
"@angular/platform-browser": "^19.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^20.0.2",
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
"@angular/router": "^20.0.2",
|
"@angular/router": "^19.2.0",
|
||||||
"@angular/service-worker": "^20.0.2",
|
|
||||||
"ngx-markdown": "^20.0.0",
|
|
||||||
"ngx-toastr": "^19.0.0",
|
"ngx-toastr": "^19.0.0",
|
||||||
"prismjs": "^1.30.0",
|
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.0.1",
|
"@angular-devkit/build-angular": "^19.2.3",
|
||||||
"@angular/cli": "^20.0.1",
|
"@angular/cli": "^19.2.3",
|
||||||
"@angular/compiler-cli": "^20.0.2",
|
"@angular/compiler-cli": "^19.2.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.6.0",
|
"jasmine-core": "~5.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
@ -39,6 +36,6 @@
|
|||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.3 KiB |
@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Chat",
|
|
||||||
"short_name": "Chat",
|
|
||||||
"display": "standalone",
|
|
||||||
"scope": "./",
|
|
||||||
"start_url": "./",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icons/icon-72x72.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-128x128.png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-384x384.png",
|
|
||||||
"sizes": "384x384",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,20 +1,15 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection, isDevMode } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
import { provideToastr } from 'ngx-toastr';
|
import { provideToastr } from 'ngx-toastr';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
import { provideMarkdown } from 'ngx-markdown';
|
|
||||||
import { provideServiceWorker } from '@angular/service-worker';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes, withInMemoryScrolling({
|
provideRouter(routes),
|
||||||
anchorScrolling: "enabled",
|
|
||||||
scrollPositionRestoration: 'top'
|
|
||||||
})),
|
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
provideToastr({
|
provideToastr({
|
||||||
@ -22,10 +17,5 @@ export const appConfig: ApplicationConfig = {
|
|||||||
positionClass: 'toast-top-right',
|
positionClass: 'toast-top-right',
|
||||||
closeButton: true
|
closeButton: true
|
||||||
}),
|
}),
|
||||||
provideMarkdown(),
|
|
||||||
provideServiceWorker('ngsw-worker.js', {
|
|
||||||
enabled: !isDevMode(),
|
|
||||||
registrationStrategy: 'registerWhenStable:30000'
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule, DatePipe } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { ChatRoutingModule } from './chat-routing.module';
|
import { ChatRoutingModule } from './chat-routing.module';
|
||||||
import { ChatComponent } from './chat/chat.component';
|
import { ChatComponent } from './chat/chat.component';
|
||||||
@ -17,8 +17,6 @@ import { MessageBarComponent } from './chat/message-bar/message-bar.component';
|
|||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
||||||
import { MarkdownModule } from 'ngx-markdown'
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -40,10 +38,7 @@ import { MarkdownModule } from 'ngx-markdown'
|
|||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule
|
||||||
MatSidenavModule,
|
|
||||||
DatePipe,
|
|
||||||
MarkdownModule
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ChatModule { }
|
export class ChatModule { }
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[matTooltip]="channel.description"
|
[matTooltip]="channel.description"
|
||||||
matTooltipPosition="right"
|
matTooltipPosition="right"
|
||||||
[matBadge]="(this.notifications != 0) ? (this.notifications > this.NOTIFICATION_LIMIT) ? `${this.NOTIFICATION_LIMIT}+` : this.notifications : ''"
|
[matBadge]="this.notifications != 0 ? this.notifications : ''"
|
||||||
matBadgeSize="medium"
|
matBadgeSize="medium"
|
||||||
[disabled]="this.selected">
|
[disabled]="this.selected">
|
||||||
{{channel.name}}
|
{{channel.name}}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
button {
|
button {
|
||||||
min-width: 80%;
|
width: 95%;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px auto;
|
margin: 5px auto;
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Channel } from '../../../../models/channel';
|
import { Channel } from '../../../../models/channel';
|
||||||
import { AudioService } from '../../../../services/audio.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-channel-entry',
|
selector: 'app-channel-entry',
|
||||||
@ -9,15 +8,11 @@ import { AudioService } from '../../../../services/audio.service';
|
|||||||
styleUrl: './channel-entry.component.scss'
|
styleUrl: './channel-entry.component.scss'
|
||||||
})
|
})
|
||||||
export class ChannelEntryComponent implements OnChanges {
|
export class ChannelEntryComponent implements OnChanges {
|
||||||
public readonly NOTIFICATION_LIMIT: number = 10;
|
|
||||||
|
|
||||||
@Input("channel") public channel!: Channel;
|
@Input("channel") public channel!: Channel;
|
||||||
@Input("selected") public selected!: boolean;
|
@Input("selected") public selected!: boolean;
|
||||||
|
|
||||||
public notifications: number = 0;
|
public notifications: number = 0;
|
||||||
|
|
||||||
constructor(private audioService: AudioService) { }
|
|
||||||
|
|
||||||
ngOnChanges(_: SimpleChanges): void {
|
ngOnChanges(_: SimpleChanges): void {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
this.notifications = 0;
|
this.notifications = 0;
|
||||||
@ -25,9 +20,6 @@ export class ChannelEntryComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Notify() {
|
public Notify() {
|
||||||
if (!this.selected) {
|
if (!this.selected) this.notifications++;
|
||||||
this.notifications++;
|
|
||||||
this.audioService.PlayNotification();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
background-color: var(--mat-sys-surface-container);
|
||||||
|
}
|
@ -10,13 +10,12 @@ import { ChannelEntryComponent } from './channel-entry/channel-entry.component';
|
|||||||
})
|
})
|
||||||
export class ChannelListComponent {
|
export class ChannelListComponent {
|
||||||
@Input('channels') public channels!: Channel[];
|
@Input('channels') public channels!: Channel[];
|
||||||
@Output("select") public selectEmitter: EventEmitter<number> = new EventEmitter<number>();
|
@Output("select") selectEmitter: EventEmitter<number> = new EventEmitter<number>();
|
||||||
@Output('close') public close: EventEmitter<void> = new EventEmitter<void>();
|
|
||||||
|
|
||||||
@ViewChildren("entry") entries!: QueryList<ChannelEntryComponent>;
|
@ViewChildren("entry") entries!: QueryList<ChannelEntryComponent>;
|
||||||
|
|
||||||
private selectedChannel?: number;
|
private selectedChannel?: number;
|
||||||
public get SelectedChannel(): number {
|
public get SelectedChannel() : number {
|
||||||
return this.selectedChannel ?? 0;
|
return this.selectedChannel ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +26,6 @@ export class ChannelListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Notify(channel: number): void {
|
public Notify(channel: number): void {
|
||||||
this.entries.find(entry => entry.channel.id == channel)?.Notify()
|
this.entries.find(entry=>entry.channel.id == channel)?.Notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,11 @@
|
|||||||
<mat-drawer-container>
|
<div id="container">
|
||||||
|
<app-toolbar />
|
||||||
|
|
||||||
<mat-drawer #drawer>
|
<app-channel-list #channelList [channels]="this.GetChannelList()" (select)="Select($event)"></app-channel-list>
|
||||||
<div id="logo">
|
|
||||||
<h1><mat-icon>chat</mat-icon>Chat</h1>
|
|
||||||
|
|
||||||
<button mat-button (click)="drawer.close()">
|
<app-feed #feed
|
||||||
<mat-icon>close</mat-icon>Close
|
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Channels</h3>
|
<app-message-bar
|
||||||
|
[channel]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].channel.id : 1"></app-message-bar>
|
||||||
<app-channel-list #channelList [channels]="this.GetChannelList()" (select)="Select($event)"
|
</div>
|
||||||
(close)="drawer.close()"></app-channel-list>
|
|
||||||
</mat-drawer>
|
|
||||||
|
|
||||||
<mat-drawer-content>
|
|
||||||
<app-toolbar (sidebar)="drawer.toggle()" />
|
|
||||||
|
|
||||||
<app-feed #feed (outOfMessages)="this.GetMoreMessages()"
|
|
||||||
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed>
|
|
||||||
|
|
||||||
<app-message-bar
|
|
||||||
[channel]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].channel.id : 1"></app-message-bar>
|
|
||||||
</mat-drawer-content>
|
|
||||||
|
|
||||||
</mat-drawer-container>
|
|
@ -1,12 +1,35 @@
|
|||||||
mat-drawer-container {
|
#container {
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-drawer-content {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 150px 1fr;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: 65px 1fr auto;
|
||||||
gap: 0px;
|
gap: 0px;
|
||||||
|
|
||||||
|
app-toolbar {
|
||||||
|
grid-column: span 2 / span 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
app-channel-list {
|
||||||
|
grid-row: span 2 / span 2;
|
||||||
|
grid-row-start: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-feed {
|
||||||
|
grid-row-start: 2;
|
||||||
|
|
||||||
|
border-left: 2px solid var(--mat-sys-surface-variant);
|
||||||
|
border-top: 2px solid var(--mat-sys-surface-variant);
|
||||||
|
border-radius: 10px 0 0 0;
|
||||||
|
|
||||||
|
background-color: var(--mat-sys-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
app-message-bar {
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-row-start: 3;
|
||||||
|
|
||||||
|
border-left: 2px solid var(--mat-sys-surface-variant);
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,7 +5,6 @@ import { ChatService } from '../../services/chat.service';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { FeedComponent } from './feed/feed.component';
|
import { FeedComponent } from './feed/feed.component';
|
||||||
import { ChannelListComponent } from './channel-list/channel-list.component';
|
import { ChannelListComponent } from './channel-list/channel-list.component';
|
||||||
import { MatDrawer } from '@angular/material/sidenav';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chat',
|
selector: 'app-chat',
|
||||||
@ -17,7 +16,6 @@ export class ChatComponent implements OnInit {
|
|||||||
public selectedChannel: number = 0;
|
public selectedChannel: number = 0;
|
||||||
public channels: { channel: Channel, messages: Message[] }[] = [];
|
public channels: { channel: Channel, messages: Message[] }[] = [];
|
||||||
|
|
||||||
@ViewChild("drawer") drawer!: MatDrawer;
|
|
||||||
@ViewChild("feed") feed!: FeedComponent;
|
@ViewChild("feed") feed!: FeedComponent;
|
||||||
@ViewChild("channelList") channelList!: ChannelListComponent;
|
@ViewChild("channelList") channelList!: ChannelListComponent;
|
||||||
|
|
||||||
@ -41,13 +39,11 @@ export class ChatComponent implements OnInit {
|
|||||||
|
|
||||||
private getMessages(): void {
|
private getMessages(): void {
|
||||||
this.channels.forEach((channelObj, i) => {
|
this.channels.forEach((channelObj, i) => {
|
||||||
this.chatService.GetMessageStream(channelObj.channel.id).subscribe({
|
this.chatService.GetMessages(channelObj.channel.id).subscribe({
|
||||||
next: message => {
|
next: messages => {
|
||||||
channelObj.messages.push(message);
|
channelObj.messages.push(messages);
|
||||||
this.feed.ScrollEventHandler();
|
this.feed.ScrollEventHandler();
|
||||||
|
this.channelList.Notify(channelObj.channel.id);
|
||||||
if (this.chatService.GetChannelLastSelected(channelObj.channel.id) < message.time)
|
|
||||||
this.channelList.Notify(channelObj.channel.id);
|
|
||||||
},
|
},
|
||||||
error: _ => {
|
error: _ => {
|
||||||
this.toastrService.error(`Failed to fetch messages for channel ${channelObj.channel.name}.`, "Error");
|
this.toastrService.error(`Failed to fetch messages for channel ${channelObj.channel.name}.`, "Error");
|
||||||
@ -56,26 +52,12 @@ export class ChatComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetMoreMessages(): void {
|
|
||||||
this.chatService.GetMessages(this.channels[this.selectedChannel].channel.id, this.channels[this.selectedChannel].messages[0]?.time ?? new Date())
|
|
||||||
.subscribe({
|
|
||||||
next: messages => {
|
|
||||||
let lastMessage = this.channels[this.selectedChannel].messages[0];
|
|
||||||
this.channels[this.selectedChannel].messages.unshift(...messages);
|
|
||||||
this.feed.ScrollToMessage(lastMessage.channel_id, lastMessage.id);
|
|
||||||
},
|
|
||||||
error: () => this.toastrService.error("Failed to fetch older messages.", "Error")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetChannelList(): Channel[] {
|
public GetChannelList(): Channel[] {
|
||||||
return this.channels.map(c => c.channel);
|
return this.channels.map(c => c.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Select(channel: number): void {
|
public Select(channel: number): void {
|
||||||
this.selectedChannel = channel;
|
this.selectedChannel = channel;
|
||||||
this.chatService.SetChannelSelected(this.channels[channel].channel.id);
|
|
||||||
this.feed.ScrollEventHandler();
|
this.feed.ScrollEventHandler();
|
||||||
this.drawer.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
<app-message *ngFor="let message of this.messages" [message]="message" />
|
<div #scrollable id="scrollable">
|
||||||
|
<app-message *ngFor="let message of this.messages" [message]="message" />
|
||||||
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
import { AfterViewInit, Component, DoCheck, ElementRef, EventEmitter, HostListener, Input, IterableDiffer, IterableDiffers, Output, QueryList, ViewChildren } from '@angular/core';
|
import { AfterViewInit, Component, DoCheck, ElementRef, EventEmitter, Input, IterableDiffer, IterableDiffers, Output } from '@angular/core';
|
||||||
import { Message } from '../../../models/message';
|
import { Message } from '../../../models/message';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -9,7 +9,6 @@ import { Message } from '../../../models/message';
|
|||||||
})
|
})
|
||||||
export class FeedComponent implements AfterViewInit {
|
export class FeedComponent implements AfterViewInit {
|
||||||
@Input('messages') public messages?: Message[];
|
@Input('messages') public messages?: Message[];
|
||||||
@Output('outOfMessages') public outOfMessages: EventEmitter<void> = new EventEmitter<void>();
|
|
||||||
|
|
||||||
constructor(private element: ElementRef<HTMLElement>) { }
|
constructor(private element: ElementRef<HTMLElement>) { }
|
||||||
|
|
||||||
@ -22,38 +21,11 @@ export class FeedComponent implements AfterViewInit {
|
|||||||
return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100;
|
return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAtTop(): boolean {
|
|
||||||
let element = this.element.nativeElement;
|
|
||||||
return element.scrollTop == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollToBottom(): void {
|
private scrollToBottom(): void {
|
||||||
setTimeout(() => this.element.nativeElement.scrollTop = this.element.nativeElement.scrollHeight, 100);
|
setTimeout(() => this.element.nativeElement.scrollTop = this.element.nativeElement.scrollHeight, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handles scroll to bottom events from the chat component when a new message arrives
|
|
||||||
public ScrollEventHandler(): void {
|
public ScrollEventHandler(): void {
|
||||||
if (this.isAtBottom()) this.scrollToBottom();
|
if (this.isAtBottom()) this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('scroll')
|
|
||||||
private detectOutOfMessages(): void {
|
|
||||||
if (this.isAtTop()) {
|
|
||||||
this.outOfMessages.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScrollToMessage(channelID: number, messageID: number): void {
|
|
||||||
setTimeout(() => {
|
|
||||||
let id = `#c${channelID}m${messageID}`;
|
|
||||||
|
|
||||||
let target = this.element.nativeElement.querySelector(id) as HTMLDivElement | null;
|
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
this.element.nativeElement.scrollTo({
|
|
||||||
top: target.offsetTop - target.offsetHeight,
|
|
||||||
behavior: "instant"
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
<div [id]="`c${message.channel_id}m${message.id}`" class="message-container">
|
<div class="message-container">
|
||||||
<app-profile-picture [username]="this.message.sender_name" />
|
<app-profile-picture [username]="this.message.sender_name" />
|
||||||
|
|
||||||
<div class="message-inner-container">
|
<div class="message-inner-container">
|
||||||
|
<div class="message-sender">{{this.message.sender_name}}</div>
|
||||||
<div class="message-sender">
|
<div class="message-content">{{this.message.content}}</div>
|
||||||
<a [matTooltip]="this.userStatus" matTooltipPosition="right"
|
|
||||||
[routerLink]="`/user/${this.message.sender_name}`">{{this.message.sender_name}}</a>
|
|
||||||
<span class="time">{{ this.message.time | date: GetMessageDateFormat() }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<markdown class="message-content" [data]="this.message.content"></markdown>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -30,24 +30,6 @@ app-profile-picture {
|
|||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
margin-right: 15px;
|
|
||||||
|
|
||||||
::ng-deep img {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 500px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: small;
|
|
||||||
color: var(--mat-sys-on-surface-variant)
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import { Component, HostListener, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Message } from '../../../../models/message';
|
import { Message } from '../../../../models/message';
|
||||||
import { UserService } from '../../../../services/user.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-message',
|
selector: 'app-message',
|
||||||
@ -10,21 +9,4 @@ import { UserService } from '../../../../services/user.service';
|
|||||||
})
|
})
|
||||||
export class MessageComponent {
|
export class MessageComponent {
|
||||||
@Input('message') public message!: Message;
|
@Input('message') public message!: Message;
|
||||||
|
|
||||||
public userStatus?: string;
|
|
||||||
|
|
||||||
constructor(private userService: UserService) { }
|
|
||||||
|
|
||||||
@HostListener('mouseenter')
|
|
||||||
private getStatus(): void {
|
|
||||||
if (!this.userStatus) {
|
|
||||||
this.userService.GetUser(this.message.sender_name).subscribe({
|
|
||||||
next: user => this.userStatus = user.status
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetMessageDateFormat(): string {
|
|
||||||
return (new Date().getDay() == this.message.time.getDay()) ? 'HH:mm' : 'MMM d, HH:mm';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
:host {
|
|
||||||
background-color: var(--mat-sys-surface-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
form {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
|
||||||
|
@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
<mat-toolbar-row>
|
<mat-toolbar-row>
|
||||||
|
|
||||||
<button id="sidebar-button" *ngIf="this.sidebar.observers.length != 0" mat-button
|
<mat-icon>chat</mat-icon>Chat
|
||||||
(click)="this.sidebar.emit()">
|
|
||||||
<mat-icon>menu</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="logo">
|
<div class="toolbar-right-side">
|
||||||
<mat-icon>chat</mat-icon>Chat
|
<button
|
||||||
|
*ngIf="this.authService.IsLoggedIn()"
|
||||||
|
(click)="Logout()"
|
||||||
|
mat-stroked-button>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button *ngIf="this.authService.IsLoggedIn()" (click)="Logout()" mat-stroked-button>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</mat-toolbar-row>
|
</mat-toolbar-row>
|
||||||
|
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
@ -2,20 +2,10 @@ mat-toolbar {
|
|||||||
background-color: var(--mat-sys-surface-container);
|
background-color: var(--mat-sys-surface-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar-button {
|
.toolbar-right-side {
|
||||||
mat-icon {
|
width: 100%;
|
||||||
width: fit-content;
|
|
||||||
margin: auto;
|
button {
|
||||||
padding: 0;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
float: right;
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -14,8 +14,6 @@ import { NgIf } from '@angular/common';
|
|||||||
styleUrl: './toolbar.component.scss'
|
styleUrl: './toolbar.component.scss'
|
||||||
})
|
})
|
||||||
export class ToolbarComponent {
|
export class ToolbarComponent {
|
||||||
@Output('sidebar') sidebar: EventEmitter<void> = new EventEmitter<void>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public authService: AuthService,
|
public authService: AuthService,
|
||||||
private toastrService: ToastrService,
|
private toastrService: ToastrService,
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { Directive, HostListener } from '@angular/core';
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: '[back-button]'
|
|
||||||
})
|
|
||||||
export class BackButtonDirective {
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
@HostListener('click')
|
|
||||||
onClick(): void {
|
|
||||||
window.history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AudioService {
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
public PlayNotification(): void {
|
|
||||||
let audio = new Audio();
|
|
||||||
audio.src = 'audio/pop.mp3'
|
|
||||||
audio.load();
|
|
||||||
audio.play();
|
|
||||||
}
|
|
||||||
}
|
|
@ -46,7 +46,6 @@ export class AuthService {
|
|||||||
let url = `${environment.apiBase}/auth/logout`;
|
let url = `${environment.apiBase}/auth/logout`;
|
||||||
|
|
||||||
localStorage.removeItem(this.LOCAL_USERNAME);
|
localStorage.removeItem(this.LOCAL_USERNAME);
|
||||||
this.loggedIn = false;
|
|
||||||
|
|
||||||
return this.http.get(url, { withCredentials: true }).pipe(
|
return this.http.get(url, { withCredentials: true }).pipe(
|
||||||
map(() => true),
|
map(() => true),
|
||||||
|
@ -14,7 +14,6 @@ export class ChatService {
|
|||||||
|
|
||||||
private readonly CHANNEL: string = "channel_id";
|
private readonly CHANNEL: string = "channel_id";
|
||||||
private readonly CONTENT: string = "content";
|
private readonly CONTENT: string = "content";
|
||||||
private readonly CHANNEL_SELECT_PREFIX: string = "ch_select";
|
|
||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
@ -37,52 +36,31 @@ export class ChatService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetMessages(channelID: number, from: Date, limit: number = 10): Observable<Message[]> {
|
public GetMessages(channelID: number): Observable<Message> {
|
||||||
let url = `${environment.apiBase}/chat/messages/${channelID}?limit=${limit}&from=${from.toISOString()}`;
|
let url = `${environment.apiBase}/chat/messages/${channelID}`;
|
||||||
return this.http.get<GetMessagesResponse>(url, { withCredentials: true })
|
let messages: Observable<Message[]> =
|
||||||
.pipe(
|
this.http.get<GetMessagesResponse>(url, { withCredentials: true })
|
||||||
map(response => {
|
.pipe(
|
||||||
if (response.error) {
|
map(response => {
|
||||||
throw new Error(response.error);
|
if (response.error) {
|
||||||
}
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.messages) {
|
if (response.messages) {
|
||||||
return response.messages;
|
return response.messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("bad API response, missing messages with no error");
|
throw new Error("bad API response, missing messages with no error");
|
||||||
}),
|
}),
|
||||||
// HttpClient doesn't parse Date objects by default
|
catchError(error => throwError(() => new Error(error.error.message)))
|
||||||
map(msgs => {
|
);
|
||||||
return msgs.map(msg => {
|
|
||||||
if (typeof msg.time == 'string')
|
|
||||||
msg.time = new Date(msg.time);
|
|
||||||
|
|
||||||
return msg;
|
url = `${environment.apiBase}/chat/subscribe/${channelID}`;
|
||||||
})
|
|
||||||
}),
|
|
||||||
catchError(error => throwError(() => new Error(error.error.message)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetMessageStream(channelID: number, historyLimit: number = 10): Observable<Message> {
|
|
||||||
let messages: Observable<Message[]> = this.GetMessages(channelID, new Date(), historyLimit);
|
|
||||||
|
|
||||||
let url = `${environment.apiBase}/chat/subscribe/${channelID}`;
|
|
||||||
let socket = webSocket<Message>(url);
|
let socket = webSocket<Message>(url);
|
||||||
|
|
||||||
return merge(
|
return merge(
|
||||||
messages
|
messages.pipe(mergeMap(msgArray => from(msgArray))),
|
||||||
.pipe(mergeMap(msgArray => from(msgArray))),
|
socket.asObservable());
|
||||||
socket.asObservable()
|
|
||||||
.pipe(map(msg => {
|
|
||||||
// HttpClient doesn't parse Date objects by default
|
|
||||||
if (typeof msg.time == 'string')
|
|
||||||
msg.time = new Date(msg.time);
|
|
||||||
|
|
||||||
return msg;
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessage(channel: number, content: string): Observable<SendMessageResponse> {
|
public SendMessage(channel: number, content: string): Observable<SendMessageResponse> {
|
||||||
@ -104,13 +82,4 @@ export class ChatService {
|
|||||||
catchError(error => throwError(() => new Error(error.error.message)))
|
catchError(error => throwError(() => new Error(error.error.message)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetChannelSelected(channel: number): void {
|
|
||||||
localStorage.setItem(`${this.CHANNEL_SELECT_PREFIX}${channel.toString()}`, new Date().toISOString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public GetChannelLastSelected(channel: number): Date {
|
|
||||||
let dateString = localStorage.getItem(`${this.CHANNEL_SELECT_PREFIX}${channel.toString()}`);
|
|
||||||
return dateString ? new Date(dateString) : new Date(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
|
|
||||||
import { UserRoutingModule } from './user-routing.module';
|
import { UserRoutingModule } from './user-routing.module';
|
||||||
import { UserComponent } from './user/user.component';
|
import { UserComponent } from './user/user.component';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { ToolbarComponent } from "../common/toolbar/toolbar.component";
|
|
||||||
import { BackButtonDirective } from '../directives/back-button.directive';
|
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -20,17 +11,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
|||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
UserRoutingModule,
|
UserRoutingModule
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatSidenavModule,
|
|
||||||
MatCardModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
ToolbarComponent,
|
|
||||||
BackButtonDirective,
|
|
||||||
MatDividerModule
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class UserModule { }
|
export class UserModule { }
|
||||||
|
@ -1,103 +1 @@
|
|||||||
<mat-drawer-container>
|
<p>{{username}}</p>
|
||||||
|
|
||||||
<mat-drawer #drawer>
|
|
||||||
<div id="logo">
|
|
||||||
<h1><mat-icon>chat</mat-icon>Chat</h1>
|
|
||||||
|
|
||||||
<button mat-button (click)="drawer.close()">
|
|
||||||
<mat-icon>close</mat-icon>Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Options</h3>
|
|
||||||
<button class="option" routerLink="/chat" mat-button>Chat</button>
|
|
||||||
<button class="option" back-button mat-button>Back</button>
|
|
||||||
|
|
||||||
</mat-drawer>
|
|
||||||
|
|
||||||
<mat-drawer-content>
|
|
||||||
|
|
||||||
<app-toolbar (sidebar)="drawer.toggle()"></app-toolbar>
|
|
||||||
|
|
||||||
<!-- display -->
|
|
||||||
<mat-card id="display-card" *ngIf="this.username != this.authService.GetUsername()">
|
|
||||||
|
|
||||||
<img *ngIf="this.user?.picture != ''" [src]="this.user?.picture" [alt]="this.username">
|
|
||||||
|
|
||||||
<h1>{{'@'}}{{this.user?.username}}</h1>
|
|
||||||
|
|
||||||
<h3>Status</h3>
|
|
||||||
<p>{{this.user?.status}}</p>
|
|
||||||
|
|
||||||
<h3>Bio</h3>
|
|
||||||
<p>{{this.user?.bio}}</p>
|
|
||||||
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
<!-- edit -->
|
|
||||||
<mat-card id="edit-card" *ngIf="this.username == this.authService.GetUsername()">
|
|
||||||
|
|
||||||
<img [src]="this.user?.picture" [alt]="this.username">
|
|
||||||
<h1>{{'@'}}{{this.user?.username}}</h1>
|
|
||||||
|
|
||||||
<form (submit)="this.ChangePassword()" [formGroup]="passwordForm">
|
|
||||||
<h3>Change password</h3>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>New password</mat-label>
|
|
||||||
<input matInput required formControlName="newPassword">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Repeat new password</mat-label>
|
|
||||||
<input matInput required formControlName="repeatNewPassword">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Current password</mat-label>
|
|
||||||
<input matInput required formControlName="password">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-button>
|
|
||||||
<mat-icon>key</mat-icon>Change password
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<mat-divider />
|
|
||||||
|
|
||||||
<form (submit)="this.ChangeStatus()" [formGroup]="statusForm">
|
|
||||||
<h3>Change status</h3>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Status</mat-label>
|
|
||||||
<textarea matInput required formControlName="status"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-button>
|
|
||||||
<mat-icon>edit</mat-icon>Change status
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<mat-divider />
|
|
||||||
|
|
||||||
<form (submit)="this.ChangeBio()" [formGroup]="bioForm">
|
|
||||||
<h3>Change bio</h3>
|
|
||||||
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Bio</mat-label>
|
|
||||||
<textarea matInput required formControlName="bio"></textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-button>
|
|
||||||
<mat-icon>edit</mat-icon>Change bio
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
</mat-drawer-content>
|
|
||||||
|
|
||||||
</mat-drawer-container>
|
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
mat-drawer-container {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
mat-drawer-content {
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
min-width: 80%;
|
|
||||||
display: block;
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card {
|
|
||||||
margin: 30px auto;
|
|
||||||
max-width: 70%;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 30%;
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--mat-sys-on-surface);
|
|
||||||
margin: 10px auto;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-card {
|
|
||||||
form {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
* {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
margin: 10px auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,5 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { User } from '../../models/user';
|
|
||||||
import { UserService } from '../../services/user.service';
|
|
||||||
import { ToastrService } from 'ngx-toastr';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { AuthService } from '../../services/auth.service';
|
|
||||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user',
|
selector: 'app-user',
|
||||||
@ -13,76 +7,12 @@ import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'
|
|||||||
templateUrl: './user.component.html',
|
templateUrl: './user.component.html',
|
||||||
styleUrl: './user.component.scss'
|
styleUrl: './user.component.scss'
|
||||||
})
|
})
|
||||||
export class UserComponent implements OnInit, OnDestroy {
|
export class UserComponent {
|
||||||
public username!: string;
|
public username!: string;
|
||||||
public user?: User;
|
|
||||||
|
|
||||||
private sub?: Subscription;
|
constructor(private route: ActivatedRoute) {
|
||||||
|
|
||||||
public passwordForm!: FormGroup;
|
|
||||||
public statusForm!: FormGroup;
|
|
||||||
public bioForm!: FormGroup;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private userService: UserService,
|
|
||||||
public authService: AuthService,
|
|
||||||
private toastrService: ToastrService,
|
|
||||||
private formBuilder: FormBuilder) {
|
|
||||||
this.route.paramMap.subscribe(params => {
|
this.route.paramMap.subscribe(params => {
|
||||||
this.username = params.get('username')!;
|
this.username = params.get('username')!;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.passwordForm = this.formBuilder.group({
|
|
||||||
newPassword: new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(6),
|
|
||||||
Validators.maxLength(32)
|
|
||||||
]),
|
|
||||||
repeatNewPassword: new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(6),
|
|
||||||
Validators.maxLength(32)
|
|
||||||
]),
|
|
||||||
password: new FormControl('', [
|
|
||||||
Validators.required
|
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
this.statusForm = this.formBuilder.group({
|
|
||||||
status: new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(0),
|
|
||||||
Validators.maxLength(200)
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.bioForm = this.formBuilder.group({
|
|
||||||
bio: new FormControl('', [
|
|
||||||
Validators.required,
|
|
||||||
Validators.minLength(0),
|
|
||||||
Validators.maxLength(300)
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sub = this.userService.GetUser(this.username)
|
|
||||||
.subscribe({
|
|
||||||
next: user => {
|
|
||||||
this.user = user;
|
|
||||||
this.statusForm.setValue({ "status": user.status });
|
|
||||||
this.bioForm.setValue({ "bio": user.bio });
|
|
||||||
},
|
|
||||||
error: () => this.toastrService.error("Failed to fectch user info.", "Error"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.sub?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChangePassword(): void { }
|
|
||||||
public ChangeStatus(): void { }
|
|
||||||
public ChangeBio(): void { }
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Chat</title>
|
<title>Chat</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<link rel="manifest" href="manifest.webmanifest">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="mat-typography">
|
<body class="mat-typography">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -22,30 +22,3 @@ body {
|
|||||||
.force-dark-mode {
|
.force-dark-mode {
|
||||||
color-scheme: dark !important;
|
color-scheme: dark !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
margin: 30px 30px 10px;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo {
|
|
||||||
margin-top: 30px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
float: right;
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
mat-icon {
|
|
||||||
width: fit-content;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|