Compare commits

..

No commits in common. "cdfa1da90a5d61e9b7aa234d1c3b341c29989c84" and "9981c634ea79bd2e2f64dfd2e6407db61f7b56f6" have entirely different histories.

36 changed files with 82 additions and 354 deletions

View File

@ -1 +1,8 @@
<mat-toolbar>
<mat-toolbar-row>
<mat-icon>chat</mat-icon>
<span>Chat</span>
</mat-toolbar-row>
</mat-toolbar>
<router-outlet /> <router-outlet />

View File

@ -1,12 +1,12 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet, MatToolbarModule, MatIconModule],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
export class AppComponent { export class AppComponent { }
}

View File

@ -13,9 +13,8 @@ export const appConfig: ApplicationConfig = {
provideHttpClient(), provideHttpClient(),
provideAnimations(), provideAnimations(),
provideToastr({ provideToastr({
timeOut: 1000, timeOut: 3000,
positionClass: 'toast-top-right', positionClass: 'toast-top-right',
closeButton: true
}), }),
] ]
}; };

View File

@ -11,6 +11,6 @@ export const routes: Routes = [
loadChildren: () => import('./chat/chat.module').then(m => m.ChatModule), loadChildren: () => import('./chat/chat.module').then(m => m.ChatModule),
canActivate: [IsLoggedInCanActivate] canActivate: [IsLoggedInCanActivate]
}, },
{ path: '', redirectTo: 'chat', pathMatch: 'full' }, { path: '', redirectTo: 'auth', pathMatch: 'full' },
{ path: '**/*', redirectTo: '', pathMatch: 'full' } { path: '**/*', redirectTo: '', pathMatch: 'full' }
]; ];

View File

@ -10,7 +10,6 @@ import { MatCardModule } from '@angular/material/card';
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 { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { ToolbarComponent } from "../common/toolbar/toolbar.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -25,8 +24,7 @@ import { ToolbarComponent } from "../common/toolbar/toolbar.component";
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatButtonModule, MatButtonModule,
ReactiveFormsModule, ReactiveFormsModule
ToolbarComponent ]
]
}) })
export class AuthModule { } export class AuthModule { }

View File

@ -1,5 +1,3 @@
<app-toolbar></app-toolbar>
<mat-card class="card"> <mat-card class="card">
<form [formGroup]="loginForm" (ngSubmit)="login()"> <form [formGroup]="loginForm" (ngSubmit)="login()">

View File

@ -35,14 +35,23 @@ export class LoginComponent implements OnInit {
).subscribe({ ).subscribe({
next: result => { next: result => {
if (result.error) { if (result.error) {
this.toastrService.error(result.error, "Error"); this.toastrService.error(result.error, "Error", {
timeOut: 3000,
closeButton: true
});
} else { } else {
this.toastrService.info(result.message, "Success"); this.toastrService.info(result.message, "Success", {
timeOut: 3000,
closeButton: true
});
this.router.navigateByUrl('chat'); this.router.navigateByUrl('chat');
} }
}, },
error: _ => { error: _ => {
this.toastrService.error("API error", "Error"); this.toastrService.error("API error", "Error", {
timeOut: 3000,
closeButton: true
});
} }
}) })
} }

View File

@ -1,5 +1,3 @@
<app-toolbar></app-toolbar>
<mat-card class="card"> <mat-card class="card">
<form [formGroup]="registerForm" (ngSubmit)="register()"> <form [formGroup]="registerForm" (ngSubmit)="register()">

View File

@ -49,14 +49,23 @@ export class RegisterComponent {
).subscribe({ ).subscribe({
next: result => { next: result => {
if (result.error) { if (result.error) {
this.toastrService.error(result.error, "Error"); this.toastrService.error(result.error, "Error", {
timeOut: 3000,
closeButton: true
});
} else { } else {
this.toastrService.info(result.message, "Success"); this.toastrService.info(result.message, "Success", {
timeOut: 3000,
closeButton: true
});
this.router.navigateByUrl('/auth/login'); this.router.navigateByUrl('/auth/login');
} }
}, },
error: _ => { error: _ => {
this.toastrService.error("API error", "Error"); this.toastrService.error("API error", "Error", {
timeOut: 3000,
closeButton: true
});
} }
}) })
} }

View File

@ -6,25 +6,18 @@ import { ChatComponent } from './chat/chat.component';
import { ChannelListComponent } from './chat/channel-list/channel-list.component'; import { ChannelListComponent } from './chat/channel-list/channel-list.component';
import { FeedComponent } from './chat/feed/feed.component'; import { FeedComponent } from './chat/feed/feed.component';
import { ChannelEntryComponent } from './chat/channel-list/channel-entry/channel-entry.component'; import { ChannelEntryComponent } from './chat/channel-list/channel-entry/channel-entry.component';
import { MessageComponent } from './chat/feed/message/message.component';
import { ProfilePictureComponent } from './chat/feed/message/profile-picture/profile-picture.component';
import { ToolbarComponent } from '../common/toolbar/toolbar.component';
import { MatBadgeModule } from '@angular/material/badge';
@NgModule({ @NgModule({
declarations: [ declarations: [
ChatComponent, ChatComponent,
ChannelListComponent, ChannelListComponent,
FeedComponent, FeedComponent,
ChannelEntryComponent, ChannelEntryComponent
MessageComponent,
ProfilePictureComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
ChatRoutingModule, ChatRoutingModule
ToolbarComponent,
MatBadgeModule
] ]
}) })
export class ChatModule { } export class ChatModule { }

View File

@ -1,7 +1 @@
<div [title]="channel.description" [class.selected]="selected"> <div [title]="channel.description" [class.selected]="selected">{{channel.name}}</div>
<span [matBadge]="this.hasAlert ? '1' : ''" matBadgeSize="small">
{{channel.name}}
</span>
</div>

View File

@ -5,6 +5,6 @@ div {
.selected { .selected {
color: var(--mat-sys-on-secondary); color: var(--mat-sys-on-secondary);
background: linear-gradient(to right, var(--mat-sys-secondary), var(--mat-sys-secondary-container) 90%, rgba(0,0,0,0)); background-color: var(--mat-sys-secondary);
transition: all .3s ease-in-out; transition: all .3s ease-in-out;
} }

View File

@ -1,4 +1,4 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Channel } from '../../../../models/channel'; import { Channel } from '../../../../models/channel';
@Component({ @Component({
@ -7,18 +7,7 @@ import { Channel } from '../../../../models/channel';
templateUrl: './channel-entry.component.html', templateUrl: './channel-entry.component.html',
styleUrl: './channel-entry.component.scss' styleUrl: './channel-entry.component.scss'
}) })
export class ChannelEntryComponent implements OnChanges { export class ChannelEntryComponent {
@Input("channel") public channel!: Channel; @Input("channel") public channel!: Channel;
@Input("selected") public selected!: boolean; @Input("selected") public selected!: boolean;
public hasAlert: boolean = false;
ngOnChanges(changes: SimpleChanges): void {
if (this.selected) {
this.hasAlert = false;
}
}
// TODO: subsribe to message alerts and display them
// unsubscirbe when leaving
} }

View File

@ -2,5 +2,5 @@
*ngFor="let channel of this.channels; let i = index" *ngFor="let channel of this.channels; let i = index"
[channel]="channels[i]" [channel]="channels[i]"
(click)="Select(i)" (click)="Select(i)"
[selected]="channels[i].id == selectedChannel?.id" [selected]="channels[i].id == selectedChannel.id"
/> />

View File

@ -1,6 +1,5 @@
import { Component, EventEmitter, OnChanges, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Channel } from '../../../models/channel'; import { Channel } from '../../../models/channel';
import { ChatService } from '../../../services/chat.service';
@Component({ @Component({
selector: 'app-channel-list', selector: 'app-channel-list',
@ -11,17 +10,27 @@ import { ChatService } from '../../../services/chat.service';
export class ChannelListComponent implements OnInit { export class ChannelListComponent implements OnInit {
@Output("select") selectEmitter: EventEmitter<Channel> = new EventEmitter<Channel>(); @Output("select") selectEmitter: EventEmitter<Channel> = new EventEmitter<Channel>();
public channels!: Channel[]; public channels: Channel[] = [
public selectedChannel?: Channel; {
id: 0,
name: 'default',
description: 'this is the default channel'
},
{
id: 1,
name: 'gaming',
description: 'this is another channel'
},
];
constructor(private chatService: ChatService) { } public selectedChannel!: Channel;
constructor() { }
ngOnInit() { ngOnInit() {
this.chatService.ListChannels() // TODO: query list of channels
.subscribe(channels => {
this.channels = channels; this.selectedChannel = this.channels[0];
this.selectedChannel = this.channels[0];
});
} }
public Select(index: number): void { public Select(index: number): void {

View File

@ -1,6 +1,4 @@
<app-toolbar />
<div id="container"> <div id="container">
<app-channel-list (select)="Select($event)"></app-channel-list> <app-channel-list (select)="Select($event)"></app-channel-list>
<app-feed [channel]="this.selectedChannel"></app-feed> <app-feed></app-feed>
</div> </div>

View File

@ -8,9 +8,7 @@ import { Channel } from '../../models/channel';
styleUrl: './chat.component.scss' styleUrl: './chat.component.scss'
}) })
export class ChatComponent { export class ChatComponent {
public selectedChannel?: Channel;
public Select(channel: Channel): void { public Select(channel: Channel): void {
this.selectedChannel = channel; // TODO: update feed
} }
} }

View File

@ -1,4 +1 @@
<app-message <p>feed works!</p>
*ngFor="let message of this.messages"
[message]="message"
/>

View File

@ -1,8 +0,0 @@
:host {
color: var(--mat-sys-on-background);
background-color: var(--mat-sys-background);
border-left: 2px solid var(--mat-sys-secondary);
border-top: 2px solid var(--mat-sys-secondary);
border-radius: 10px 0;
}

View File

@ -1,8 +1,4 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Message } from '../../../models/message';
import { ChatService } from '../../../services/chat.service';
import { UserService } from '../../../services/user.service';
import { Channel } from '../../../models/channel';
@Component({ @Component({
selector: 'app-feed', selector: 'app-feed',
@ -10,18 +6,6 @@ import { Channel } from '../../../models/channel';
templateUrl: './feed.component.html', templateUrl: './feed.component.html',
styleUrl: './feed.component.scss' styleUrl: './feed.component.scss'
}) })
export class FeedComponent implements OnChanges { export class FeedComponent {
private readonly DEFAULT_CHANNEL_ID: number = 1;
@Input('channel') public channel?: Channel;
public messages!: Message[];
constructor(private chatService: ChatService, private userService: UserService) { }
ngOnChanges() {
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID)
.subscribe(messages => {
this.messages = messages;
});
}
} }

View File

@ -1,9 +0,0 @@
<div class="message-container">
<app-profile-picture [username]="this.message.sender" />
<div class="message-inner-container">
<div class="message-sender">{{this.message.sender}}</div>
<div class="message-content">{{this.message.content}}</div>
</div>
</div>

View File

@ -1,35 +0,0 @@
app-profile-picture {
display: inline-block;
width: 50px;
height: 50px;
margin: 10px;
}
.message-container {
display: grid;
grid-template-columns: 70px 1fr;
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
.message-inner-container {
margin-top: 10px;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1.5rem 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
height: fit-content;
.message-sender {
vertical-align: top;
}
.message-content {
height: fit-content;
}
}
}

View File

@ -1,12 +0,0 @@
import { Component, Input } from '@angular/core';
import { Message } from '../../../../models/message';
@Component({
selector: 'app-message',
standalone: false,
templateUrl: './message.component.html',
styleUrl: './message.component.scss'
})
export class MessageComponent {
@Input('message') public message!: Message;
}

View File

@ -1 +0,0 @@
<img [src]="this.url" [alt]="this.username">

View File

@ -1,7 +0,0 @@
img {
display: inline-block;
width: 100%;
border-radius: 50%;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 1);
}

View File

@ -1,20 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { UserService } from '../../../../../services/user.service';
@Component({
selector: 'app-profile-picture',
standalone: false,
templateUrl: './profile-picture.component.html',
styleUrl: './profile-picture.component.scss'
})
export class ProfilePictureComponent implements OnInit {
@Input("username") public username!: string;
public url?: string;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.userService.GetProfilePictureURL(this.username)
.subscribe(url => this.url = url);
}
}

View File

@ -1,18 +0,0 @@
<mat-toolbar>
<mat-toolbar-row>
<mat-icon>chat</mat-icon>Chat
<div class="toolbar-right-side">
<button
*ngIf="this.authService.IsLoggedIn()"
(click)="Logout()"
mat-stroked-button>
Logout
</button>
</div>
</mat-toolbar-row>
</mat-toolbar>

View File

@ -1,7 +0,0 @@
.toolbar-right-side {
width: 100%;
button {
float: right;
}
}

View File

@ -1,35 +0,0 @@
import { Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { AuthService } from '../../services/auth.service';
import { ToastrService } from 'ngx-toastr';
import { Router } from '@angular/router';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-toolbar',
imports: [MatToolbarModule, MatIconModule, MatButtonModule, NgIf],
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.scss'
})
export class ToolbarComponent {
constructor(
public authService: AuthService,
private toastrService: ToastrService,
private router: Router) { }
public Logout(): void {
this.authService.Logout()
.subscribe({
next: result => {
if (result) {
this.router.navigateByUrl('auth/login');
this.toastrService.info("Successfully logged out", "Logout");
} else {
this.toastrService.error("Logout failed", "Error");
}
}
});
}
}

View File

@ -1,9 +0,0 @@
import { Timestamp } from "rxjs"
export class Message {
public id!: number
public sender!: string
public channel!: number
public time!: Date
public content!: string
}

View File

@ -1,6 +0,0 @@
export class User {
public username!: string;
public status!: string;
public picture!: string;
public bio!: string;
}

View File

@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs'; import { catchError, map, Observable, of } from 'rxjs';
import { LoginResponse, RegisterResponse } from './responses/auth'; import { LoginResponse, RegisterResponse } from './responses/auth';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -71,10 +71,5 @@ export const IsLoggedInCanActivate: CanActivateFn = (
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot state: RouterStateSnapshot
) => { ) => {
if (inject(AuthService).IsLoggedIn()) { return inject(AuthService).IsLoggedIn();
return true;
} else {
inject(Router).navigateByUrl("auth/login");
return false;
}
} }

View File

@ -1,56 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Channel } from '../models/channel';
import { Message } from '../models/message';
@Injectable({
providedIn: 'root'
})
export class ChatService {
constructor() { }
// TODO: implement
public ListChannels(): Observable<Channel[]> {
return new Observable<Channel[]>(subscriber => {
subscriber.next([
{
id: 0,
name: 'default',
description: 'this is the default channel'
},
{
id: 1,
name: 'gaming',
description: 'this is another channel'
},
]);
subscriber.complete();
});
}
// TODO: implement
// TODO: refactor this so it first returns the n last messages,
// then listens for incoming messages and forwards them as they come
public GetMessages(channelID: number): Observable<Message[]> {
return new Observable<Message[]>(subscriber => {
subscriber.next([
{
id: 1,
sender: 'Test User 1',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 2,
sender: 'Test User 2',
channel: 2,
time: new Date(),
content: 'this is my second message'
}
]);
subscriber.complete();
});
}
}

View File

@ -1,7 +1,10 @@
import { APIResponse } from "./basic"; export class LoginResponse {
public message?: string;
export class LoginResponse extends APIResponse { public error?: string;
public session?: string; public token?: string;
} }
export class RegisterResponse extends APIResponse { } export class RegisterResponse {
public message?: string;
public error?: string;
}

View File

@ -1,4 +0,0 @@
export class APIResponse {
public message?: string;
public error?: string;
}

View File

@ -1,23 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../models/user';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
// TODO: implement
public GetProfilePictureURL(username: string): Observable<string> {
return new Observable<string>(subscriber => {
subscriber.next("https://i.pinimg.com/736x/00/70/16/00701602b0eac0390b3107b9e2a665e0.jpg");
subscriber.complete();
});
}
// TODO: implement
public GetUser(username: string): Observable<User> {
throw new Error('Not implemented');
}
}