From c81143ecdc729f2c2e26377ba0ca1d01deee5d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 7 Dec 2025 04:21:11 +0100 Subject: [PATCH] More webapp changes+fixes --- webapp/src/app/app.config.ts | 12 +++ webapp/src/app/core/models/message.model.ts | 1 + webapp/src/app/core/services/api.service.ts | 1 + .../channel-detail.component.html | 26 ++++++- .../channel-detail.component.scss | 3 + .../channel-detail.component.ts | 29 ++++++++ .../channel-list/channel-list.component.html | 42 ++++++++++- .../channel-list/channel-list.component.ts | 73 ++++++++++++++++++- .../keys/key-detail/key-detail.component.scss | 3 + .../keys/key-detail/key-detail.component.ts | 11 ++- .../message-detail.component.html | 4 +- .../message-detail.component.ts | 11 ++- .../message-list/message-list.component.scss | 3 + .../subscription-detail.component.html | 22 +++++- .../subscription-detail.component.ts | 20 +++++ .../subscription-list.component.html | 27 +++++++ .../subscription-list.component.ts | 29 ++++++++ 17 files changed, 297 insertions(+), 20 deletions(-) diff --git a/webapp/src/app/app.config.ts b/webapp/src/app/app.config.ts index 5cf9097..2e90a87 100644 --- a/webapp/src/app/app.config.ts +++ b/webapp/src/app/app.config.ts @@ -39,6 +39,12 @@ import { InfoCircleOutline, ExclamationCircleOutline, CheckCircleOutline, + UserAddOutline, + UserDeleteOutline, + PauseCircleOutline, + PlayCircleOutline, + StopOutline, + ArrowLeftOutline, } from '@ant-design/icons-angular/icons'; import { routes } from './app.routes'; @@ -79,6 +85,12 @@ const icons: IconDefinition[] = [ InfoCircleOutline, ExclamationCircleOutline, CheckCircleOutline, + UserAddOutline, + UserDeleteOutline, + PauseCircleOutline, + PlayCircleOutline, + StopOutline, + ArrowLeftOutline, ]; export const appConfig: ApplicationConfig = { diff --git a/webapp/src/app/core/models/message.model.ts b/webapp/src/app/core/models/message.model.ts index 38669ff..91dac1f 100644 --- a/webapp/src/app/core/models/message.model.ts +++ b/webapp/src/app/core/models/message.model.ts @@ -23,6 +23,7 @@ export interface MessageListParams { search?: string; sender?: string[]; subscription_status?: 'all' | 'confirmed' | 'unconfirmed'; + used_key?: string; trimmed?: boolean; page_size?: number; next_page_token?: string; diff --git a/webapp/src/app/core/services/api.service.ts b/webapp/src/app/core/services/api.service.ts index 60d15b8..b1c9088 100644 --- a/webapp/src/app/core/services/api.service.ts +++ b/webapp/src/app/core/services/api.service.ts @@ -153,6 +153,7 @@ export class ApiService { } } if (params.subscription_status) httpParams = httpParams.set('subscription_status', params.subscription_status); + if (params.used_key) httpParams = httpParams.set('used_key', params.used_key); if (params.trimmed !== undefined) httpParams = httpParams.set('trimmed', params.trimmed); if (params.page_size) httpParams = httpParams.set('page_size', params.page_size); if (params.next_page_token) httpParams = httpParams.set('next_page_token', params.next_page_token); diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.html b/webapp/src/app/features/channels/channel-detail/channel-detail.component.html index 5197614..7f67c1d 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.html +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.html @@ -150,7 +150,21 @@ @if (isOwner()) { - + + + @if (expertMode()) { + + } + Subscriber Status + Active Created Actions @@ -184,6 +199,13 @@ + + + + {{ sub.active ? 'Active' : 'Inactive' }} + + +
{{ sub.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
@@ -234,7 +256,7 @@ } @empty { - + diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss b/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss index f54f263..5b399c3 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss @@ -104,6 +104,9 @@ .message-content { font-size: 12px; color: #666; + white-space: pre; + max-height: 2lh; + overflow-y: clip; } .text-muted { diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts b/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts index 84f6e91..a0a9606 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts @@ -390,4 +390,33 @@ export class ChannelDetailComponent implements OnInit { } }); } + + isUserSubscribed(): boolean { + return this.channel()?.subscription !== null; + } + + toggleSelfSubscription(): void { + const channel = this.channel(); + const userId = this.authService.getUserId(); + if (!channel || !userId) return; + + if (this.isUserSubscribed()) { + // Unsubscribe + const subscriptionId = channel.subscription!.subscription_id; + this.apiService.deleteSubscription(userId, subscriptionId).subscribe({ + next: () => { + this.notification.success('Unsubscribed from channel'); + this.loadChannel(channel.channel_id); + } + }); + } else { + // Subscribe + this.apiService.createSubscription(userId, { channel_id: channel.channel_id }).subscribe({ + next: () => { + this.notification.success('Subscribed to channel'); + this.loadChannel(channel.channel_id); + } + }); + } + } } diff --git a/webapp/src/app/features/channels/channel-list/channel-list.component.html b/webapp/src/app/features/channels/channel-list/channel-list.component.html index 9e5d254..7a586f8 100644 --- a/webapp/src/app/features/channels/channel-list/channel-list.component.html +++ b/webapp/src/app/features/channels/channel-list/channel-list.component.html @@ -9,6 +9,21 @@ + + + + + + + @if (getTabDescription()) { + + } + Subscribers Messages Last Sent + @if (expertMode()) { + Actions + } @@ -56,10 +74,12 @@ @if (isOwned(channel)) {
- {{ getOwnerDisplayName(channel.owner_user_id) }} +
{{ getOwnerDisplayName(channel.owner_user_id) }}
+
{{ channel.owner_user_id }}
} @else { - {{ getOwnerDisplayName(channel.owner_user_id) }} +
{{ getOwnerDisplayName(channel.owner_user_id) }}
+
{{ channel.owner_user_id }}
} @@ -112,10 +132,26 @@ } } + @if (expertMode()) { + + @if (isOwned(channel)) { + + } + + } } @empty { - + diff --git a/webapp/src/app/features/channels/channel-list/channel-list.component.ts b/webapp/src/app/features/channels/channel-list/channel-list.component.ts index a1093d9..d896afa 100644 --- a/webapp/src/app/features/channels/channel-list/channel-list.component.ts +++ b/webapp/src/app/features/channels/channel-list/channel-list.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, OnInit } from '@angular/core'; +import { Component, inject, signal, computed, OnInit } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { NzTableModule } from 'ng-zorro-antd/table'; @@ -9,13 +9,19 @@ import { NzBadgeModule } from 'ng-zorro-antd/badge'; import { NzEmptyModule } from 'ng-zorro-antd/empty'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; +import { NzTabsModule } from 'ng-zorro-antd/tabs'; +import { NzAlertModule } from 'ng-zorro-antd/alert'; 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 } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; import { ChannelSubscribersComponent } from '../channel-subscribers/channel-subscribers.component'; +type ChannelTab = 'all' | 'owned' | 'foreign'; + @Component({ selector: 'app-channel-list', standalone: true, @@ -31,6 +37,8 @@ import { ChannelSubscribersComponent } from '../channel-subscribers/channel-subs NzEmptyModule, NzCardModule, NzToolTipModule, + NzTabsModule, + NzAlertModule, RelativeTimePipe, ChannelSubscribersComponent, ], @@ -40,12 +48,31 @@ import { ChannelSubscribersComponent } from '../channel-subscribers/channel-subs export class ChannelListComponent implements OnInit { private apiService = inject(ApiService); private authService = inject(AuthService); + private notification = inject(NotificationService); + private settingsService = inject(SettingsService); private userCacheService = inject(UserCacheService); private router = inject(Router); - channels = signal([]); + allChannels = signal([]); ownerNames = signal>(new Map()); loading = signal(false); + expertMode = this.settingsService.expertMode; + activeTab = signal('all'); + + channels = computed(() => { + const userId = this.authService.getUserId(); + const all = this.allChannels(); + const tab = this.activeTab(); + + switch (tab) { + case 'owned': + return all.filter(c => c.owner_user_id === userId); + case 'foreign': + return all.filter(c => c.owner_user_id !== userId); + default: + return all; + } + }); ngOnInit(): void { this.loadChannels(); @@ -58,7 +85,7 @@ export class ChannelListComponent implements OnInit { this.loading.set(true); this.apiService.getChannels(userId, 'all_any').subscribe({ next: (response) => { - this.channels.set(response.channels); + this.allChannels.set(response.channels); this.loading.set(false); this.resolveOwnerNames(response.channels); }, @@ -68,6 +95,22 @@ export class ChannelListComponent implements OnInit { }); } + onTabChange(index: number): void { + const tabs: ChannelTab[] = ['all', 'owned', 'foreign']; + this.activeTab.set(tabs[index]); + } + + getTabDescription(): string | null { + switch (this.activeTab()) { + case 'owned': + return 'Channels that you own and can configure.'; + case 'foreign': + return 'Channels owned by other users that you are subscribed to.'; + default: + return null; + } + } + private resolveOwnerNames(channels: ChannelWithSubscription[]): void { const uniqueOwnerIds = [...new Set(channels.map(c => c.owner_user_id))]; for (const ownerId of uniqueOwnerIds) { @@ -109,4 +152,28 @@ export class ChannelListComponent implements OnInit { return { label: 'Not Subscribed', color: 'default' }; } + + toggleSelfSubscription(channel: ChannelWithSubscription, event: Event): void { + event.stopPropagation(); + const userId = this.authService.getUserId(); + if (!userId) return; + + if (channel.subscription) { + // Unsubscribe + this.apiService.deleteSubscription(userId, channel.subscription.subscription_id).subscribe({ + next: () => { + this.notification.success('Unsubscribed from channel'); + this.loadChannels(); + } + }); + } else { + // Subscribe + this.apiService.createSubscription(userId, { channel_id: channel.channel_id }).subscribe({ + next: () => { + this.notification.success('Subscribed to channel'); + this.loadChannels(); + } + }); + } + } } diff --git a/webapp/src/app/features/keys/key-detail/key-detail.component.scss b/webapp/src/app/features/keys/key-detail/key-detail.component.scss index c2ce331..4817daa 100644 --- a/webapp/src/app/features/keys/key-detail/key-detail.component.scss +++ b/webapp/src/app/features/keys/key-detail/key-detail.component.scss @@ -121,6 +121,9 @@ .message-content { font-size: 12px; color: #666; + white-space: pre; + max-height: 2lh; + overflow-y: clip; } .cell-name { diff --git a/webapp/src/app/features/keys/key-detail/key-detail.component.ts b/webapp/src/app/features/keys/key-detail/key-detail.component.ts index fdfa2cf..66680ce 100644 --- a/webapp/src/app/features/keys/key-detail/key-detail.component.ts +++ b/webapp/src/app/features/keys/key-detail/key-detail.component.ts @@ -131,17 +131,16 @@ export class KeyDetailComponent implements OnInit { loadMessages(keyId: string, nextPageToken?: string): void { this.loadingMessages.set(true); - // Load more messages than page size to ensure we get enough after filtering this.apiService.getMessages({ - page_size: 64, + subscription_status: 'all', + used_key: keyId, + page_size: this.messagesPageSize, next_page_token: nextPageToken, trimmed: true }).subscribe({ next: (response) => { - // Filter messages by the key that was used to send them - const filtered = response.messages.filter(m => m.used_key_id === keyId); - this.messages.set(filtered.slice(0, this.messagesPageSize)); - this.messagesTotalCount.set(filtered.length); + this.messages.set(response.messages); + this.messagesTotalCount.set(response.total_count); this.messagesNextPageToken.set(response.next_page_token || null); this.loadingMessages.set(false); }, diff --git a/webapp/src/app/features/messages/message-detail/message-detail.component.html b/webapp/src/app/features/messages/message-detail/message-detail.component.html index b8d9acc..13c0efc 100644 --- a/webapp/src/app/features/messages/message-detail/message-detail.component.html +++ b/webapp/src/app/features/messages/message-detail/message-detail.component.html @@ -101,9 +101,7 @@ @for (delivery of deliveriesTable.data; track delivery.delivery_id) { - - {{ delivery.receiver_client_id }} - + {{ delivery.receiver_client_id }} diff --git a/webapp/src/app/features/messages/message-detail/message-detail.component.ts b/webapp/src/app/features/messages/message-detail/message-detail.component.ts index 14bf412..a42b770 100644 --- a/webapp/src/app/features/messages/message-detail/message-detail.component.ts +++ b/webapp/src/app/features/messages/message-detail/message-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, OnInit, computed } from '@angular/core'; +import { Component, inject, signal, OnInit, computed, effect } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { NzCardModule } from 'ng-zorro-antd/card'; @@ -66,6 +66,15 @@ export class MessageDetailComponent implements OnInit { showDeliveries = computed(() => this.expertMode() && this.isChannelOwner()); + constructor() { + // Watch for expert mode changes and load deliveries when it becomes visible + effect(() => { + if (this.showDeliveries() && this.message() && this.deliveries().length === 0 && !this.loadingDeliveries()) { + this.loadDeliveries(this.message()!.message_id); + } + }); + } + ngOnInit(): void { const messageId = this.route.snapshot.paramMap.get('id'); if (messageId) { diff --git a/webapp/src/app/features/messages/message-list/message-list.component.scss b/webapp/src/app/features/messages/message-list/message-list.component.scss index ea5b47a..17cfc91 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.scss +++ b/webapp/src/app/features/messages/message-list/message-list.component.scss @@ -50,6 +50,9 @@ .message-content { font-size: 12px; color: #666; + white-space: pre; + max-height: 2lh; + overflow-y: clip; } .text-muted { diff --git a/webapp/src/app/features/subscriptions/subscription-detail/subscription-detail.component.html b/webapp/src/app/features/subscriptions/subscription-detail/subscription-detail.component.html index 368db00..5bfc386 100644 --- a/webapp/src/app/features/subscriptions/subscription-detail/subscription-detail.component.html +++ b/webapp/src/app/features/subscriptions/subscription-detail/subscription-detail.component.html @@ -16,7 +16,7 @@ Accept } - @if (subscription()!.confirmed && isOwner()) { + @if (subscription()!.confirmed && isOwner() && !isOwnSubscription()) { + } @else { + + } + } + @if (expertMode() && isOutgoing() && subscription()!.confirmed && subscription()!.active && !isOwnSubscription()) { } @else { + + @if (isOwnSubscription(sub)) { + @if (sub.active) { + + } @else { + + } + }