diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico index 57614f9..3729f2e 100644 Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ diff --git a/webapp/src/app/features/account/account-info/account-info.component.html b/webapp/src/app/features/account/account-info/account-info.component.html index 95207cb..85edba8 100644 --- a/webapp/src/app/features/account/account-info/account-info.component.html +++ b/webapp/src/app/features/account/account-info/account-info.component.html @@ -78,33 +78,6 @@ - -
- -
- - - -
-

Deleting your account will permanently remove all your data including messages, channels, subscriptions, and keys.

- -
-
} diff --git a/webapp/src/app/features/account/account-info/account-info.component.ts b/webapp/src/app/features/account/account-info/account-info.component.ts index a69f305..d863c2a 100644 --- a/webapp/src/app/features/account/account-info/account-info.component.ts +++ b/webapp/src/app/features/account/account-info/account-info.component.ts @@ -1,6 +1,5 @@ import { Component, inject, signal, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzButtonModule } from 'ng-zorro-antd/button'; @@ -9,7 +8,6 @@ import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions'; import { NzTagModule } from 'ng-zorro-antd/tag'; import { NzSpinModule } from 'ng-zorro-antd/spin'; import { NzProgressModule } from 'ng-zorro-antd/progress'; -import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; import { NzModalModule } from 'ng-zorro-antd/modal'; import { NzFormModule } from 'ng-zorro-antd/form'; import { NzInputModule } from 'ng-zorro-antd/input'; @@ -33,7 +31,6 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; NzTagModule, NzSpinModule, NzProgressModule, - NzPopconfirmModule, NzModalModule, NzFormModule, NzInputModule, @@ -47,11 +44,9 @@ export class AccountInfoComponent implements OnInit { private apiService = inject(ApiService); private authService = inject(AuthService); private notification = inject(NotificationService); - private router = inject(Router); user = signal(null); loading = signal(true); - deleting = signal(false); // Edit username modal showEditModal = signal(false); @@ -121,28 +116,4 @@ export class AccountInfoComponent implements OnInit { } }); } - - // Logout - logout(): void { - this.authService.logout(); - this.router.navigate(['/login']); - } - - // Delete account - deleteAccount(): void { - const userId = this.authService.getUserId(); - if (!userId) return; - - this.deleting.set(true); - this.apiService.deleteUser(userId).subscribe({ - next: () => { - this.notification.success('Account deleted'); - this.authService.logout(); - this.router.navigate(['/login']); - }, - error: () => { - this.deleting.set(false); - } - }); - } } diff --git a/webapp/src/app/features/keys/key-list/key-list.component.html b/webapp/src/app/features/keys/key-list/key-list.component.html index 1e54741..acdcd82 100644 --- a/webapp/src/app/features/keys/key-list/key-list.component.html +++ b/webapp/src/app/features/keys/key-list/key-list.component.html @@ -168,10 +168,11 @@ } diff --git a/webapp/src/app/features/keys/key-list/key-list.component.scss b/webapp/src/app/features/keys/key-list/key-list.component.scss index 39f6692..3de064b 100644 --- a/webapp/src/app/features/keys/key-list/key-list.component.scss +++ b/webapp/src/app/features/keys/key-list/key-list.component.scss @@ -60,7 +60,17 @@ label { display: flex; align-items: center; - gap: 4px; + margin-left: 0; + } + + nz-tag { + width: 32px; + text-align: center; + margin-right: 8px; + } + + .perm-label { + min-width: 100px; } .perm-desc { diff --git a/webapp/src/app/features/keys/key-list/key-list.component.ts b/webapp/src/app/features/keys/key-list/key-list.component.ts index d7653c6..3378f54 100644 --- a/webapp/src/app/features/keys/key-list/key-list.component.ts +++ b/webapp/src/app/features/keys/key-list/key-list.component.ts @@ -184,7 +184,10 @@ export class KeyListComponent implements OnInit { onPermissionChange(perm: TokenPermission, checked: boolean): void { if (checked) { - if (!this.newKeyPermissions.includes(perm)) { + if (perm === 'A') { + // Admin selected - clear other permissions + this.newKeyPermissions = ['A']; + } else if (!this.newKeyPermissions.includes(perm)) { this.newKeyPermissions = [...this.newKeyPermissions, perm]; } } else { diff --git a/webapp/src/app/features/messages/message-list/message-list.component.html b/webapp/src/app/features/messages/message-list/message-list.component.html index 81c1028..31398fc 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.html +++ b/webapp/src/app/features/messages/message-list/message-list.component.html @@ -7,52 +7,43 @@ - -
- - - - - - - - - - - - - + - @if (searchText || priorityFilter !== null || channelFilter) { - + @if (hasActiveFilters()) { +
+ @if (appliedSearchText) { + + "{{ appliedSearchText }}" + } + @for (channel of channelFilter; track channel) { + + {{ getChannelDisplayName(channel) }} + + } + @if (priorityFilter.length > 0) { + + {{ getPriorityLabel(+priorityFilter[0]) }} + + } + Clear all
- + } Title - Channel + Channel Sender - Priority + Priority Time 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 9b48ec0..99868a6 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 @@ -9,10 +9,28 @@ } } -.filter-card { +.search-bar { margin-bottom: 16px; } +.active-filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; + + nz-tag { + max-width: none; + } + + .clear-all { + margin-left: 8px; + font-size: 12px; + cursor: pointer; + } +} + .message-title { font-weight: 500; color: #333; diff --git a/webapp/src/app/features/messages/message-list/message-list.component.ts b/webapp/src/app/features/messages/message-list/message-list.component.ts index 9a27f1c..e9422bc 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.ts +++ b/webapp/src/app/features/messages/message-list/message-list.component.ts @@ -2,10 +2,9 @@ import { Component, inject, signal, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; -import { NzTableModule } from 'ng-zorro-antd/table'; +import { NzTableModule, NzTableFilterList } from 'ng-zorro-antd/table'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzInputModule } from 'ng-zorro-antd/input'; -import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzTagModule } from 'ng-zorro-antd/tag'; import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzEmptyModule } from 'ng-zorro-antd/empty'; @@ -26,7 +25,6 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; NzTableModule, NzButtonModule, NzInputModule, - NzSelectModule, NzTagModule, NzIconModule, NzEmptyModule, @@ -49,13 +47,39 @@ export class MessageListComponent implements OnInit { // Filters searchText = ''; - priorityFilter: number | null = null; - channelFilter = ''; + appliedSearchText = ''; + priorityFilter: string[] = []; + channelFilter: string[] = []; + + // Filter options + priorityFilters: NzTableFilterList = [ + { text: 'Low', value: '0' }, + { text: 'Normal', value: '1' }, + { text: 'High', value: '2' }, + ]; + channelFilters = signal([]); ngOnInit(): void { + this.loadChannels(); this.loadMessages(); } + loadChannels(): void { + const userId = this.authService.getUserId(); + if (!userId) return; + + this.apiService.getChannels(userId, 'all_any').subscribe({ + next: (response) => { + this.channelFilters.set( + response.channels.map(ch => ({ + text: ch.display_name, + value: ch.internal_name, + })) + ); + } + }); + } + loadMessages(append = false): void { this.loading.set(true); @@ -64,14 +88,14 @@ export class MessageListComponent implements OnInit { trimmed: true, }; - if (this.searchText) { - params.search = this.searchText; + if (this.appliedSearchText) { + params.search = this.appliedSearchText; } - if (this.priorityFilter !== null) { - params.priority = this.priorityFilter; + if (this.priorityFilter.length === 1) { + params.priority = parseInt(this.priorityFilter[0], 10); } - if (this.channelFilter) { - params.channel = this.channelFilter; + if (this.channelFilter.length > 0) { + params.channel = this.channelFilter.join(','); } if (append && this.nextPageToken()) { params.next_page_token = this.nextPageToken()!; @@ -98,16 +122,59 @@ export class MessageListComponent implements OnInit { } applyFilters(): void { + this.appliedSearchText = this.searchText; this.loadMessages(); } - clearFilters(): void { - this.searchText = ''; - this.priorityFilter = null; - this.channelFilter = ''; + onPriorityFilterChange(filters: string[] | null): void { + this.priorityFilter = filters ?? []; this.loadMessages(); } + onChannelFilterChange(filters: string[] | null): void { + this.channelFilter = filters ?? []; + this.loadMessages(); + } + + clearSearch(): void { + this.searchText = ''; + this.appliedSearchText = ''; + this.loadMessages(); + } + + clearChannelFilter(): void { + this.channelFilter = []; + this.loadMessages(); + } + + removeChannelFilter(channel: string): void { + this.channelFilter = this.channelFilter.filter(c => c !== channel); + this.loadMessages(); + } + + clearPriorityFilter(): void { + this.priorityFilter = []; + this.loadMessages(); + } + + clearAllFilters(): void { + this.searchText = ''; + this.appliedSearchText = ''; + this.channelFilter = []; + this.priorityFilter = []; + this.loadMessages(); + } + + hasActiveFilters(): boolean { + return !!this.appliedSearchText || this.channelFilter.length > 0 || this.priorityFilter.length > 0; + } + + getChannelDisplayName(internalName: string): string { + const filters = this.channelFilters(); + const channel = filters.find(f => f.value === internalName); + return channel?.text?.toString() ?? internalName; + } + loadMore(): void { if (this.nextPageToken()) { this.loadMessages(true); diff --git a/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.html b/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.html index 4c693bd..3d329d8 100644 --- a/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.html +++ b/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.html @@ -16,10 +16,21 @@ - + + + + @if (getTabDescription()) { + + } + - Direction + Type Channel Subscriber Owner @@ -44,8 +55,8 @@ @for (sub of subscriptions(); track sub.subscription_id) { - - {{ getDirectionLabel(sub) }} + + {{ getTypeLabel(sub).label }} diff --git a/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.ts b/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.ts index 7b18447..9091fa0 100644 --- a/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.ts +++ b/webapp/src/app/features/subscriptions/subscription-list/subscription-list.component.ts @@ -13,6 +13,7 @@ import { NzModalModule } from 'ng-zorro-antd/modal'; import { NzFormModule } from 'ng-zorro-antd/form'; import { NzInputModule } from 'ng-zorro-antd/input'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; +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'; @@ -20,7 +21,19 @@ import { UserCacheService, ResolvedUser } from '../../../core/services/user-cach import { Subscription, SubscriptionFilter } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; -type TabDirection = 'both' | 'outgoing' | 'incoming'; +type SubscriptionTab = 'all' | 'own' | 'deactivated' | 'external' | 'incoming'; + +interface TabConfig { + filter: SubscriptionFilter; +} + +const TAB_CONFIGS: Record = { + all: { filter: {} }, + own: { filter: { direction: 'outgoing', confirmation: 'confirmed', external: 'false' } }, + deactivated: { filter: { direction: 'outgoing', confirmation: 'unconfirmed', external: 'false' } }, + external: { filter: { direction: 'outgoing', confirmation: 'all', external: 'true' } }, + incoming: { filter: { direction: 'incoming', confirmation: 'all', external: 'true' } }, +}; @Component({ selector: 'app-subscription-list', @@ -40,6 +53,7 @@ type TabDirection = 'both' | 'outgoing' | 'incoming'; NzFormModule, NzInputModule, NzToolTipModule, + NzAlertModule, RelativeTimePipe, ], templateUrl: './subscription-list.component.html', @@ -54,7 +68,7 @@ export class SubscriptionListComponent implements OnInit { subscriptions = signal([]); userNames = signal>(new Map()); loading = signal(false); - direction: TabDirection = 'both'; + activeTab: SubscriptionTab = 'all'; // Create subscription modal showCreateModal = signal(false); @@ -72,10 +86,7 @@ export class SubscriptionListComponent implements OnInit { this.loading.set(true); - const filter: SubscriptionFilter = {}; - if (this.direction !== 'both') { - filter.direction = this.direction; - } + const filter = TAB_CONFIGS[this.activeTab].filter; this.apiService.getSubscriptions(userId, filter).subscribe({ next: (response) => { @@ -108,8 +119,8 @@ export class SubscriptionListComponent implements OnInit { } onTabChange(index: number): void { - const directions: TabDirection[] = ['both', 'outgoing', 'incoming']; - this.direction = directions[index]; + const tabs: SubscriptionTab[] = ['all', 'own', 'deactivated', 'external', 'incoming']; + this.activeTab = tabs[index]; this.loadSubscriptions(); } @@ -199,10 +210,29 @@ export class SubscriptionListComponent implements OnInit { return { label: 'Pending', color: 'orange' }; } - getDirectionLabel(sub: Subscription): string { - if (this.isOutgoing(sub)) { - return 'Outgoing'; + getTypeLabel(sub: Subscription): { label: string; color: string } { + const userId = this.authService.getUserId(); + if (sub.subscriber_user_id === sub.channel_owner_user_id) { + return { label: 'Own', color: 'green' }; + } + if (sub.subscriber_user_id === userId) { + return { label: 'External', color: 'blue' }; + } + return { label: 'Incoming', color: 'purple' }; + } + + getTabDescription(): string | null { + switch (this.activeTab) { + case 'own': + return 'Active subscriptions to your channels.'; + case 'deactivated': + return 'Deactivated subscriptions to your channels. These can be reactivated by you.'; + case 'external': + return 'Your subscriptions to channels owned by other users.'; + case 'incoming': + return 'Subscription from other users to your channels.'; + default: + return null; } - return 'Incoming'; } }