More webapp changes+fixes
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -150,7 +150,21 @@
|
||||
</nz-card>
|
||||
|
||||
@if (isOwner()) {
|
||||
<nz-card nzTitle="Subscriptions" class="mt-16">
|
||||
<nz-card nzTitle="Subscriptions" [nzExtra]="subscriptionsCardExtra" class="mt-16">
|
||||
<ng-template #subscriptionsCardExtra>
|
||||
@if (expertMode()) {
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
[nzType]="isUserSubscribed() ? 'default' : 'primary'"
|
||||
nz-tooltip
|
||||
[nzTooltipTitle]="isUserSubscribed() ? 'Unsubscribe' : 'Subscribe'"
|
||||
(click)="toggleSelfSubscription()"
|
||||
>
|
||||
<span nz-icon [nzType]="isUserSubscribed() ? 'user-delete' : 'user-add'"></span>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
<nz-table
|
||||
#subscriptionTable
|
||||
[nzData]="subscriptions()"
|
||||
@@ -164,6 +178,7 @@
|
||||
<tr>
|
||||
<th>Subscriber</th>
|
||||
<th nzWidth="0">Status</th>
|
||||
<th nzWidth="0">Active</th>
|
||||
<th nzWidth="0">Created</th>
|
||||
<th nzWidth="0">Actions</th>
|
||||
</tr>
|
||||
@@ -184,6 +199,13 @@
|
||||
</nz-tag>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="cell-link" [routerLink]="['/subscriptions', sub.subscription_id]">
|
||||
<nz-tag [nzColor]="sub.active ? 'green' : 'default'">
|
||||
{{ sub.active ? 'Active' : 'Inactive' }}
|
||||
</nz-tag>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="cell-link" [routerLink]="['/subscriptions', sub.subscription_id]">
|
||||
<div class="timestamp-absolute">{{ sub.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}</div>
|
||||
@@ -234,7 +256,7 @@
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<td colspan="5">
|
||||
<nz-empty nzNotFoundContent="No subscriptions"></nz-empty>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
.message-content {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre;
|
||||
max-height: 2lh;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nz-tabset (nzSelectedIndexChange)="onTabChange($event)">
|
||||
<nz-tab nzTitle="All"></nz-tab>
|
||||
<nz-tab nzTitle="Owned"></nz-tab>
|
||||
<nz-tab nzTitle="Foreign"></nz-tab>
|
||||
</nz-tabset>
|
||||
|
||||
@if (getTabDescription()) {
|
||||
<nz-alert
|
||||
nzType="info"
|
||||
[nzMessage]="getTabDescription()!"
|
||||
nzShowIcon
|
||||
style="margin-bottom: 16px;"
|
||||
></nz-alert>
|
||||
}
|
||||
|
||||
<nz-card>
|
||||
<nz-table
|
||||
#channelTable
|
||||
@@ -28,6 +43,9 @@
|
||||
<th style="width: 400px">Subscribers</th>
|
||||
<th style="width: 0">Messages</th>
|
||||
<th style="width: 0">Last Sent</th>
|
||||
@if (expertMode()) {
|
||||
<th style="width: 0">Actions</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -56,10 +74,12 @@
|
||||
<td>
|
||||
@if (isOwned(channel)) {
|
||||
<a class="cell-link" [routerLink]="['/channels', channel.channel_id]">
|
||||
{{ getOwnerDisplayName(channel.owner_user_id) }}
|
||||
<div class="channel-name">{{ getOwnerDisplayName(channel.owner_user_id) }}</div>
|
||||
<div class="channel-id mono">{{ channel.owner_user_id }}</div>
|
||||
</a>
|
||||
} @else {
|
||||
{{ getOwnerDisplayName(channel.owner_user_id) }}
|
||||
<div class="channel-name">{{ getOwnerDisplayName(channel.owner_user_id) }}</div>
|
||||
<div class="channel-id mono">{{ channel.owner_user_id }}</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@@ -112,10 +132,26 @@
|
||||
}
|
||||
}
|
||||
</td>
|
||||
@if (expertMode()) {
|
||||
<td>
|
||||
@if (isOwned(channel)) {
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
[nzType]="channel.subscription ? 'default' : 'primary'"
|
||||
nz-tooltip
|
||||
[nzTooltipTitle]="channel.subscription ? 'Unsubscribe' : 'Subscribe'"
|
||||
(click)="toggleSelfSubscription(channel, $event)"
|
||||
>
|
||||
<span nz-icon [nzType]="channel.subscription ? 'user-delete' : 'user-add'"></span>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td [attr.colspan]="expertMode() ? 8 : 7">
|
||||
<nz-empty nzNotFoundContent="No channels found"></nz-empty>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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<ChannelWithSubscription[]>([]);
|
||||
allChannels = signal<ChannelWithSubscription[]>([]);
|
||||
ownerNames = signal<Map<string, ResolvedUser>>(new Map());
|
||||
loading = signal(false);
|
||||
expertMode = this.settingsService.expertMode;
|
||||
activeTab = signal<ChannelTab>('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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@
|
||||
.message-content {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre;
|
||||
max-height: 2lh;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.cell-name {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -101,9 +101,7 @@
|
||||
@for (delivery of deliveriesTable.data; track delivery.delivery_id) {
|
||||
<tr>
|
||||
<td>
|
||||
<a [routerLink]="['/clients', delivery.receiver_client_id]" class="mono">
|
||||
{{ delivery.receiver_client_id }}
|
||||
</a>
|
||||
<span class="mono">{{ delivery.receiver_client_id }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<nz-tag [nzColor]="getStatusColor(delivery.status)">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
.message-content {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre;
|
||||
max-height: 2lh;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
Accept
|
||||
</button>
|
||||
}
|
||||
@if (subscription()!.confirmed && isOwner()) {
|
||||
@if (subscription()!.confirmed && isOwner() && !isOwnSubscription()) {
|
||||
<button
|
||||
nz-button
|
||||
nz-popconfirm
|
||||
@@ -27,7 +27,25 @@
|
||||
Deactivate
|
||||
</button>
|
||||
}
|
||||
@if (expertMode() && isOutgoing() && subscription()!.confirmed && subscription()!.active) {
|
||||
@if (isOwnSubscription()) {
|
||||
@if (subscription()!.active) {
|
||||
<button
|
||||
nz-button
|
||||
nz-popconfirm
|
||||
nzPopconfirmTitle="Deactivate this subscription?"
|
||||
(nzOnConfirm)="setInactive()"
|
||||
>
|
||||
<span nz-icon nzType="pause-circle"></span>
|
||||
Deactivate
|
||||
</button>
|
||||
} @else {
|
||||
<button nz-button nzType="primary" (click)="activateSubscription()">
|
||||
<span nz-icon nzType="play-circle"></span>
|
||||
Activate
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (expertMode() && isOutgoing() && subscription()!.confirmed && subscription()!.active && !isOwnSubscription()) {
|
||||
<button
|
||||
nz-button
|
||||
nz-popconfirm
|
||||
|
||||
@@ -118,6 +118,13 @@ export class SubscriptionDetailComponent implements OnInit {
|
||||
return sub.channel_owner_user_id === userId;
|
||||
}
|
||||
|
||||
isOwnSubscription(): boolean {
|
||||
const sub = this.subscription();
|
||||
if (!sub) return false;
|
||||
const userId = this.authService.getUserId();
|
||||
return sub.subscriber_user_id === userId && sub.channel_owner_user_id === userId;
|
||||
}
|
||||
|
||||
getStatusInfo(): { label: string; color: string } {
|
||||
const sub = this.subscription();
|
||||
if (!sub) return { label: 'Unknown', color: 'default' };
|
||||
@@ -153,6 +160,19 @@ export class SubscriptionDetailComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
activateSubscription(): void {
|
||||
const sub = this.subscription();
|
||||
const userId = this.authService.getUserId();
|
||||
if (!sub || !userId) return;
|
||||
|
||||
this.apiService.confirmSubscription(userId, sub.subscription_id, { active: true }).subscribe({
|
||||
next: (updated) => {
|
||||
this.subscription.set(updated);
|
||||
this.notification.success('Subscription activated');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deactivateSubscription(): void {
|
||||
const sub = this.subscription();
|
||||
const userId = this.authService.getUserId();
|
||||
|
||||
@@ -133,6 +133,33 @@
|
||||
<span nz-icon nzType="close"></span>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Own subscriptions: can activate/deactivate -->
|
||||
@if (isOwnSubscription(sub)) {
|
||||
@if (sub.active) {
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
nz-tooltip
|
||||
nzTooltipTitle="Deactivate"
|
||||
nz-popconfirm
|
||||
nzPopconfirmTitle="Deactivate this subscription?"
|
||||
(nzOnConfirm)="deactivateSubscription(sub)"
|
||||
>
|
||||
<span nz-icon nzType="pause-circle"></span>
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
nz-button
|
||||
nzSize="small"
|
||||
nzType="primary"
|
||||
nz-tooltip
|
||||
nzTooltipTitle="Activate"
|
||||
(click)="activateSubscription(sub)"
|
||||
>
|
||||
<span nz-icon nzType="play-circle"></span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<!-- Confirmed or outgoing: can revoke -->
|
||||
<button
|
||||
nz-button
|
||||
|
||||
@@ -159,6 +159,11 @@ export class SubscriptionListComponent implements OnInit {
|
||||
return sub.channel_owner_user_id === userId;
|
||||
}
|
||||
|
||||
isOwnSubscription(sub: Subscription): boolean {
|
||||
const userId = this.authService.getUserId();
|
||||
return sub.subscriber_user_id === userId && sub.channel_owner_user_id === userId;
|
||||
}
|
||||
|
||||
viewSubscription(sub: Subscription): void {
|
||||
this.router.navigate(['/subscriptions', sub.subscription_id]);
|
||||
}
|
||||
@@ -200,6 +205,30 @@ export class SubscriptionListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
activateSubscription(sub: Subscription): void {
|
||||
const userId = this.authService.getUserId();
|
||||
if (!userId) return;
|
||||
|
||||
this.apiService.confirmSubscription(userId, sub.subscription_id, { active: true }).subscribe({
|
||||
next: () => {
|
||||
this.notification.success('Subscription activated');
|
||||
this.loadSubscriptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deactivateSubscription(sub: Subscription): void {
|
||||
const userId = this.authService.getUserId();
|
||||
if (!userId) return;
|
||||
|
||||
this.apiService.confirmSubscription(userId, sub.subscription_id, { active: false }).subscribe({
|
||||
next: () => {
|
||||
this.notification.success('Subscription deactivated');
|
||||
this.loadSubscriptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create subscription
|
||||
openCreateModal(): void {
|
||||
this.newChannelOwner = '';
|
||||
|
||||
Reference in New Issue
Block a user