Files
SimpleCloudNotifier/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts
Mike Schwörer 2b7950f5dc
All checks were successful
Build Docker and Deploy / Build Docker Container (push) Successful in 1m41s
Build Docker and Deploy / Run Unit-Tests (push) Successful in 9m31s
Build Docker and Deploy / Deploy to Server (push) Successful in 18s
More webapp changes+fixes
2025-12-05 21:39:32 +01:00

394 lines
12 KiB
TypeScript

import { Component, inject, signal, computed, OnInit } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service';
import { NotificationService } from '../../../core/services/notification.service';
import { SettingsService } from '../../../core/services/settings.service';
import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service';
import { ChannelWithSubscription, Subscription, Message } from '../../../core/models';
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive';
import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component';
import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid';
@Component({
selector: 'app-channel-detail',
standalone: true,
imports: [
CommonModule,
DatePipe,
FormsModule,
RouterLink,
NzCardModule,
NzButtonModule,
NzIconModule,
NzTagModule,
NzSpinModule,
NzPopconfirmModule,
NzDividerModule,
NzInputModule,
NzModalModule,
NzFormModule,
NzTableModule,
NzToolTipModule,
NzEmptyModule,
NzPaginationModule,
RelativeTimePipe,
CopyToClipboardDirective,
QrCodeDisplayComponent,
MetadataGridComponent,
MetadataValueComponent,
],
templateUrl: './channel-detail.component.html',
styleUrl: './channel-detail.component.scss'
})
export class ChannelDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private apiService = inject(ApiService);
private authService = inject(AuthService);
private notification = inject(NotificationService);
private settingsService = inject(SettingsService);
private userCacheService = inject(UserCacheService);
channel = signal<ChannelWithSubscription | null>(null);
subscriptions = signal<Subscription[]>([]);
messages = signal<Message[]>([]);
userNames = signal<Map<string, ResolvedUser>>(new Map());
loading = signal(true);
loadingSubscriptions = signal(false);
loadingMessages = signal(false);
deleting = signal(false);
expertMode = this.settingsService.expertMode;
// Messages pagination
messagesPageSize = 16;
messagesNextPageToken = signal<string | null>(null);
messagesTotalCount = signal(0);
messagesCurrentPage = signal(1);
// Edit modal
showEditModal = signal(false);
editDisplayName = '';
editDescription = '';
saving = signal(false);
// QR code data (computed from channel)
qrCodeData = computed(() => {
const channel = this.channel();
if (!channel || !channel.subscribe_key) return '';
return [
'@scn.channel.subscribe',
'v1',
channel.display_name,
channel.owner_user_id,
channel.channel_id,
channel.subscribe_key
].join('\n');
});
ngOnInit(): void {
const channelId = this.route.snapshot.paramMap.get('id');
if (channelId) {
this.loadChannel(channelId);
}
}
loadChannel(channelId: string): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.loading.set(true);
this.apiService.getChannel(userId, channelId).subscribe({
next: (channel) => {
this.channel.set(channel);
this.loading.set(false);
if (this.isOwner()) {
this.loadSubscriptions(channelId);
}
this.loadMessages(channelId);
},
error: () => {
this.loading.set(false);
}
});
}
loadSubscriptions(channelId: string): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.loadingSubscriptions.set(true);
this.apiService.getChannelSubscriptions(userId, channelId).subscribe({
next: (response) => {
this.subscriptions.set(response.subscriptions);
this.loadingSubscriptions.set(false);
this.resolveUserNames(response.subscriptions);
},
error: () => {
this.loadingSubscriptions.set(false);
}
});
}
loadMessages(channelId: string, nextPageToken?: string): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.loadingMessages.set(true);
this.apiService.getChannelMessages(userId, channelId, {
page_size: this.messagesPageSize,
next_page_token: nextPageToken,
trimmed: true
}).subscribe({
next: (response) => {
this.messages.set(response.messages);
this.messagesNextPageToken.set(response.next_page_token || null);
this.messagesTotalCount.set(response.total_count);
this.loadingMessages.set(false);
},
error: () => {
this.loadingMessages.set(false);
}
});
}
messagesGoToPage(page: number): void {
const channel = this.channel();
if (!channel) return;
this.messagesCurrentPage.set(page);
// For pagination with tokens, we need to handle this differently
// The API uses next_page_token, so we'll reload from the beginning for now
// In a real implementation, you'd need to track tokens per page or use offset-based pagination
if (page === 1) {
this.loadMessages(channel.channel_id);
} else {
// For simplicity, use the next page token if going forward
const token = this.messagesNextPageToken();
if (token) {
this.loadMessages(channel.channel_id, token);
}
}
}
viewMessage(message: Message): void {
this.router.navigate(['/messages', message.message_id]);
}
getPriorityColor(priority: number): string {
switch (priority) {
case 0: return 'default';
case 1: return 'blue';
case 2: return 'orange';
default: return 'default';
}
}
getPriorityLabel(priority: number): string {
switch (priority) {
case 0: return 'Low';
case 1: return 'Normal';
case 2: return 'High';
default: return 'Unknown';
}
}
private resolveUserNames(subscriptions: Subscription[]): void {
const userIds = new Set<string>();
for (const sub of subscriptions) {
userIds.add(sub.subscriber_user_id);
}
for (const id of userIds) {
this.userCacheService.resolveUser(id).subscribe(resolved => {
this.userNames.update(map => new Map(map).set(id, resolved));
});
}
}
getUserDisplayName(userId: string): string {
const resolved = this.userNames().get(userId);
return resolved?.displayName || userId;
}
goBack(): void {
this.router.navigate(['/channels']);
}
isOwner(): boolean {
const channel = this.channel();
const userId = this.authService.getUserId();
return channel?.owner_user_id === userId;
}
// Edit methods
openEditModal(): void {
const channel = this.channel();
if (!channel) return;
this.editDisplayName = channel.display_name;
this.editDescription = channel.description_name || '';
this.showEditModal.set(true);
}
closeEditModal(): void {
this.showEditModal.set(false);
}
saveChannel(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.saving.set(true);
this.apiService.updateChannel(userId, channel.channel_id, {
display_name: this.editDisplayName,
description_name: this.editDescription || undefined
}).subscribe({
next: (updated) => {
this.channel.set(updated);
this.notification.success('Channel updated');
this.closeEditModal();
this.saving.set(false);
},
error: () => {
this.saving.set(false);
}
});
}
// Regenerate keys
regenerateSubscribeKey(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.apiService.updateChannel(userId, channel.channel_id, {
subscribe_key: 'true'
}).subscribe({
next: (updated) => {
this.channel.set(updated);
this.notification.success('Subscribe key regenerated');
}
});
}
regenerateSendKey(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.apiService.updateChannel(userId, channel.channel_id, {
send_key: 'true'
}).subscribe({
next: (updated) => {
this.channel.set(updated);
this.notification.success('Send key regenerated');
}
});
}
getSubscriptionStatus(): { label: string; color: string } {
const channel = this.channel();
if (!channel) return { label: 'Unknown', color: 'default' };
if (this.isOwner()) {
if (channel.subscription) {
return { label: 'Owned & Subscribed', color: 'green' };
}
return { label: 'Owned', color: 'blue' };
}
if (channel.subscription) {
if (channel.subscription.confirmed) {
return { label: 'Subscribed', color: 'green' };
}
return { label: 'Pending', color: 'orange' };
}
return { label: 'Not Subscribed', color: 'default' };
}
deleteChannel(): void {
const channel = this.channel();
const userId = this.authService.getUserId();
if (!channel || !userId) return;
this.deleting.set(true);
this.apiService.deleteChannel(userId, channel.channel_id).subscribe({
next: () => {
this.notification.success('Channel deleted');
this.router.navigate(['/channels']);
},
error: () => {
this.deleting.set(false);
}
});
}
viewSubscription(sub: Subscription): void {
this.router.navigate(['/subscriptions', sub.subscription_id]);
}
acceptSubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.confirmSubscription(userId, sub.subscription_id, { confirmed: true }).subscribe({
next: () => {
this.notification.success('Subscription accepted');
const channel = this.channel();
if (channel) {
this.loadSubscriptions(channel.channel_id);
}
}
});
}
denySubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.deleteSubscription(userId, sub.subscription_id).subscribe({
next: () => {
this.notification.success('Subscription denied');
const channel = this.channel();
if (channel) {
this.loadSubscriptions(channel.channel_id);
}
}
});
}
revokeSubscription(sub: Subscription): void {
const userId = this.authService.getUserId();
if (!userId) return;
this.apiService.deleteSubscription(userId, sub.subscription_id).subscribe({
next: () => {
this.notification.success('Subscription revoked');
const channel = this.channel();
if (channel) {
this.loadSubscriptions(channel.channel_id);
}
}
});
}
}