Compare commits
2 Commits
ebbaf6716d
...
a393bd91ab
Author | SHA1 | Date | |
---|---|---|---|
a393bd91ab | |||
6d690b5043 |
@ -11,6 +11,11 @@ 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: 'user',
|
||||||
|
loadChildren: () => import('./user/user.module').then(m => m.UserModule),
|
||||||
|
canActivate: [IsLoggedInCanActivate]
|
||||||
|
},
|
||||||
{ path: '', redirectTo: 'chat', pathMatch: 'full' },
|
{ path: '', redirectTo: 'chat', pathMatch: 'full' },
|
||||||
{ path: '**/*', redirectTo: '', pathMatch: 'full' }
|
{ path: '**', redirectTo: 'chat', pathMatch: 'full' }
|
||||||
];
|
];
|
||||||
|
@ -6,7 +6,8 @@ import { RegisterComponent } from './register/register.component';
|
|||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: 'login', component: LoginComponent },
|
{ path: 'login', component: LoginComponent },
|
||||||
{ path: 'register', component: RegisterComponent },
|
{ path: 'register', component: RegisterComponent },
|
||||||
{ path: '', redirectTo: 'login', pathMatch: 'full' }
|
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||||
|
{ path: '**', redirectTo: 'login', pathMatch: "full" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -4,6 +4,7 @@ import { ChatComponent } from './chat/chat.component';
|
|||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: ChatComponent },
|
{ path: '', component: ChatComponent },
|
||||||
|
{ path: '**', redirectTo: '', pathMatch: "full" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
[matTooltip]="channel.description"
|
[matTooltip]="channel.description"
|
||||||
matTooltipPosition="right"
|
matTooltipPosition="right"
|
||||||
[matBadge]="this.hasAlert ? '1' : ''"
|
[matBadge]="this.notifications != 0 ? this.notifications : ''"
|
||||||
matBadgeSize="medium"
|
matBadgeSize="medium"
|
||||||
[disabled]="this.selected">
|
[disabled]="this.selected">
|
||||||
{{channel.name}}
|
{{channel.name}}
|
||||||
|
@ -11,14 +11,15 @@ export class ChannelEntryComponent implements OnChanges {
|
|||||||
@Input("channel") public channel!: Channel;
|
@Input("channel") public channel!: Channel;
|
||||||
@Input("selected") public selected!: boolean;
|
@Input("selected") public selected!: boolean;
|
||||||
|
|
||||||
public hasAlert: boolean = false;
|
public notifications: number = 0;
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(_: SimpleChanges): void {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
this.hasAlert = false;
|
this.notifications = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: subsribe to message alerts and display them
|
public Notify() {
|
||||||
// unsubscirbe when leaving
|
if (!this.selected) this.notifications++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<app-channel-entry
|
<ng-container *ngIf="this.channels && this.channels.length != 0">
|
||||||
*ngFor="let channel of this.channels; let i = index"
|
<app-channel-entry #entry *ngFor="let channel of this.channels; let i = index" [channel]="channel" (click)="Select(i)"
|
||||||
[channel]="channels[i]"
|
[selected]="i == SelectedChannel" />
|
||||||
(click)="Select(i)"
|
</ng-container>
|
||||||
[selected]="channels[i].id == selectedChannel?.id"
|
|
||||||
/>
|
|
@ -1,7 +1,6 @@
|
|||||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
|
||||||
import { Channel } from '../../../models/channel';
|
import { Channel } from '../../../models/channel';
|
||||||
import { ChatService } from '../../../services/chat.service';
|
import { ChannelEntryComponent } from './channel-entry/channel-entry.component';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-channel-list',
|
selector: 'app-channel-list',
|
||||||
@ -9,31 +8,24 @@ import { Subject, takeUntil } from 'rxjs';
|
|||||||
templateUrl: './channel-list.component.html',
|
templateUrl: './channel-list.component.html',
|
||||||
styleUrl: './channel-list.component.scss'
|
styleUrl: './channel-list.component.scss'
|
||||||
})
|
})
|
||||||
export class ChannelListComponent implements OnInit, OnDestroy {
|
export class ChannelListComponent {
|
||||||
@Output("select") selectEmitter: EventEmitter<Channel> = new EventEmitter<Channel>();
|
@Input('channels') public channels!: Channel[];
|
||||||
|
@Output("select") selectEmitter: EventEmitter<number> = new EventEmitter<number>();
|
||||||
|
|
||||||
public channels!: Channel[];
|
@ViewChildren("entry") entries!: QueryList<ChannelEntryComponent>;
|
||||||
public selectedChannel?: Channel;
|
|
||||||
|
|
||||||
private destroy = new Subject<void>();
|
private selectedChannel?: number;
|
||||||
|
public get SelectedChannel() : number {
|
||||||
constructor(private chatService: ChatService) { }
|
return this.selectedChannel ?? 0;
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.chatService.ListAvailableChannels()
|
|
||||||
.pipe(takeUntil(this.destroy))
|
|
||||||
.subscribe(channels => {
|
|
||||||
this.channels = channels;
|
|
||||||
this.selectedChannel = this.channels[0];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
constructor() { }
|
||||||
this.destroy.next();
|
|
||||||
this.destroy.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Select(index: number): void {
|
public Select(index: number): void {
|
||||||
this.selectEmitter.emit(this.selectedChannel = this.channels[index]);
|
this.selectEmitter.emit(this.selectedChannel = index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Notify(channel: number): void {
|
||||||
|
this.entries.find(entry=>entry.channel.id == channel)?.Notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<div id="container">
|
<div id="container">
|
||||||
<app-toolbar />
|
<app-toolbar />
|
||||||
|
|
||||||
<app-channel-list (select)="Select($event)"></app-channel-list>
|
<app-channel-list #channelList [channels]="this.GetChannelList()" (select)="Select($event)"></app-channel-list>
|
||||||
|
|
||||||
<app-feed [channel]="this.selectedChannel"></app-feed>
|
<app-feed #feed
|
||||||
|
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed>
|
||||||
|
|
||||||
<app-message-bar [channel]="this.selectedChannel?.id ?? 1"></app-message-bar>
|
<app-message-bar
|
||||||
|
[channel]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].channel.id : 1"></app-message-bar>
|
||||||
</div>
|
</div>
|
@ -1,5 +1,10 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { Channel } from '../../models/channel';
|
import { Channel } from '../../models/channel';
|
||||||
|
import { Message } from '../../models/message';
|
||||||
|
import { ChatService } from '../../services/chat.service';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { FeedComponent } from './feed/feed.component';
|
||||||
|
import { ChannelListComponent } from './channel-list/channel-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chat',
|
selector: 'app-chat',
|
||||||
@ -7,10 +12,52 @@ import { Channel } from '../../models/channel';
|
|||||||
templateUrl: './chat.component.html',
|
templateUrl: './chat.component.html',
|
||||||
styleUrl: './chat.component.scss'
|
styleUrl: './chat.component.scss'
|
||||||
})
|
})
|
||||||
export class ChatComponent {
|
export class ChatComponent implements OnInit {
|
||||||
public selectedChannel?: Channel;
|
public selectedChannel: number = 0;
|
||||||
|
public channels: { channel: Channel, messages: Message[] }[] = [];
|
||||||
|
|
||||||
public Select(channel: Channel): void {
|
@ViewChild("feed") feed!: FeedComponent;
|
||||||
|
@ViewChild("channelList") channelList!: ChannelListComponent;
|
||||||
|
|
||||||
|
constructor(private chatService: ChatService, private toastrService: ToastrService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.getChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChannels(): void {
|
||||||
|
this.chatService.ListAvailableChannels().subscribe({
|
||||||
|
next: channels => {
|
||||||
|
this.channels = channels.map(channel => { return { channel: channel, messages: [] as Message[] } });
|
||||||
|
this.getMessages();
|
||||||
|
},
|
||||||
|
error: _ => {
|
||||||
|
this.toastrService.error("Failed to fetch channels.", "Error");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMessages(): void {
|
||||||
|
this.channels.forEach((channelObj, i) => {
|
||||||
|
this.chatService.GetMessages(channelObj.channel.id).subscribe({
|
||||||
|
next: messages => {
|
||||||
|
channelObj.messages.push(messages);
|
||||||
|
this.feed.ScrollEventHandler();
|
||||||
|
this.channelList.Notify(channelObj.channel.id);
|
||||||
|
},
|
||||||
|
error: _ => {
|
||||||
|
this.toastrService.error(`Failed to fetch messages for channel ${channelObj.channel.name}.`, "Error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetChannelList(): Channel[] {
|
||||||
|
return this.channels.map(c => c.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Select(channel: number): void {
|
||||||
this.selectedChannel = channel;
|
this.selectedChannel = channel;
|
||||||
|
this.feed.ScrollEventHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { AfterContentInit, AfterViewChecked, AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } 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';
|
||||||
import { ChatService } from '../../../services/chat.service';
|
|
||||||
import { Channel } from '../../../models/channel';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-feed',
|
selector: 'app-feed',
|
||||||
@ -10,41 +7,15 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './feed.component.html',
|
templateUrl: './feed.component.html',
|
||||||
styleUrl: './feed.component.scss'
|
styleUrl: './feed.component.scss'
|
||||||
})
|
})
|
||||||
export class FeedComponent implements OnChanges, OnDestroy, AfterViewInit {
|
export class FeedComponent implements AfterViewInit {
|
||||||
private readonly DEFAULT_CHANNEL_ID: number = 1;
|
@Input('messages') public messages?: Message[];
|
||||||
|
|
||||||
@Input('channel') public channel?: Channel;
|
constructor(private element: ElementRef<HTMLElement>) { }
|
||||||
public messages: Message[] = [];
|
|
||||||
|
|
||||||
public subscription?: Subscription;
|
|
||||||
|
|
||||||
constructor(private chatService: ChatService, private element: ElementRef<HTMLElement>) { }
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
|
||||||
if (this.subscription) {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
this.messages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subscription =
|
|
||||||
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID)
|
|
||||||
.subscribe(message => {
|
|
||||||
this.messages.push(message);
|
|
||||||
|
|
||||||
if (this.isAtBottom()) this.scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.subscription) {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAtBottom(): boolean {
|
private isAtBottom(): boolean {
|
||||||
let element = this.element.nativeElement;
|
let element = this.element.nativeElement;
|
||||||
return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100;
|
return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100;
|
||||||
@ -53,4 +24,8 @@ export class FeedComponent implements OnChanges, OnDestroy, AfterViewInit {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ScrollEventHandler(): void {
|
||||||
|
if (this.isAtBottom()) this.scrollToBottom();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ export class AuthService {
|
|||||||
private readonly USERNAME_FIELD: string = "username";
|
private readonly USERNAME_FIELD: string = "username";
|
||||||
private readonly PASSWORD_FIELD: string = "password";
|
private readonly PASSWORD_FIELD: string = "password";
|
||||||
private readonly REPEAT_PASSWORD_FIELD: string = "repeatPassword";
|
private readonly REPEAT_PASSWORD_FIELD: string = "repeatPassword";
|
||||||
|
private readonly LOCAL_USERNAME: string = "last_username";
|
||||||
|
|
||||||
private loggedIn: boolean = false;
|
private loggedIn: boolean = false;
|
||||||
|
|
||||||
@ -25,7 +26,9 @@ export class AuthService {
|
|||||||
formData.append(this.USERNAME_FIELD, username);
|
formData.append(this.USERNAME_FIELD, username);
|
||||||
formData.append(this.PASSWORD_FIELD, password);
|
formData.append(this.PASSWORD_FIELD, password);
|
||||||
|
|
||||||
return this.http.post<LoginResponse>(url, formData, { withCredentials: true })
|
localStorage.setItem(this.LOCAL_USERNAME, username);
|
||||||
|
|
||||||
|
return this.http.post<LoginResponse>(url, formData, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
public Register(username: string, password: string, repeatPassword: string): Observable<RegisterResponse> {
|
public Register(username: string, password: string, repeatPassword: string): Observable<RegisterResponse> {
|
||||||
@ -42,6 +45,8 @@ export class AuthService {
|
|||||||
public Logout(): Observable<boolean> {
|
public Logout(): Observable<boolean> {
|
||||||
let url = `${environment.apiBase}/auth/logout`;
|
let url = `${environment.apiBase}/auth/logout`;
|
||||||
|
|
||||||
|
localStorage.removeItem(this.LOCAL_USERNAME);
|
||||||
|
|
||||||
return this.http.get(url, { withCredentials: true }).pipe(
|
return this.http.get(url, { withCredentials: true }).pipe(
|
||||||
map(() => true),
|
map(() => true),
|
||||||
catchError(() => of(false))
|
catchError(() => of(false))
|
||||||
@ -70,6 +75,11 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IsLoggedIn(): boolean { return this.loggedIn; }
|
public IsLoggedIn(): boolean { return this.loggedIn; }
|
||||||
|
public GetUsername(): string {
|
||||||
|
let username = localStorage.getItem(this.LOCAL_USERNAME)
|
||||||
|
if (username) return username;
|
||||||
|
else throw new Error("not logged in");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IsLoggedInCanActivate: CanActivateFn = (
|
export const IsLoggedInCanActivate: CanActivateFn = (
|
||||||
@ -89,4 +99,19 @@ export const IsLoggedInCanActivate: CanActivateFn = (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UsernameResolver: CanActivateFn = (
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
__: RouterStateSnapshot
|
||||||
|
): boolean => {
|
||||||
|
const authService = inject(AuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
try {
|
||||||
|
router.navigateByUrl(`user/${authService.GetUsername()}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
router.navigateByUrl("auth/login");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
19
src/app/user/user-routing.module.ts
Normal file
19
src/app/user/user-routing.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { UserComponent } from './user/user.component';
|
||||||
|
import { UsernameResolver } from '../services/auth.service';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: ':username', component: UserComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '', canActivate: [UsernameResolver], component: UserComponent, pathMatch: "full"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class UserRoutingModule { }
|
17
src/app/user/user.module.ts
Normal file
17
src/app/user/user.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { UserRoutingModule } from './user-routing.module';
|
||||||
|
import { UserComponent } from './user/user.component';
|
||||||
|
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
UserComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
UserRoutingModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserModule { }
|
1
src/app/user/user/user.component.html
Normal file
1
src/app/user/user/user.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>{{username}}</p>
|
0
src/app/user/user/user.component.scss
Normal file
0
src/app/user/user/user.component.scss
Normal file
18
src/app/user/user/user.component.ts
Normal file
18
src/app/user/user/user.component.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-user',
|
||||||
|
standalone: false,
|
||||||
|
templateUrl: './user.component.html',
|
||||||
|
styleUrl: './user.component.scss'
|
||||||
|
})
|
||||||
|
export class UserComponent {
|
||||||
|
public username!: string;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute) {
|
||||||
|
this.route.paramMap.subscribe(params => {
|
||||||
|
this.username = params.get('username')!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user