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(null); subscriptions = signal([]); messages = signal([]); userNames = signal>(new Map()); loading = signal(true); loadingSubscriptions = signal(false); loadingMessages = signal(false); deleting = signal(false); expertMode = this.settingsService.expertMode; // Messages pagination messagesPageSize = 16; messagesNextPageToken = signal(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(); 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); } } }); } }