diff --git a/webapp/angular.json b/webapp/angular.json
index cbb6394..0dc0d1a 100644
--- a/webapp/angular.json
+++ b/webapp/angular.json
@@ -48,10 +48,7 @@
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
- {
- "glob": "**/*",
- "input": "public"
- }
+ {"input": "src/assets", "output": ".", "glob": "**/*" }
],
"styles": [
"src/styles.scss"
@@ -96,30 +93,6 @@
}
},
"defaultConfiguration": "development"
- },
- "extract-i18n": {
- "builder": "@angular-devkit/build-angular:extract-i18n"
- },
- "test": {
- "builder": "@angular-devkit/build-angular:karma",
- "options": {
- "polyfills": [
- "zone.js",
- "zone.js/testing"
- ],
- "tsConfig": "tsconfig.spec.json",
- "inlineStyleLanguage": "scss",
- "assets": [
- {
- "glob": "**/*",
- "input": "public"
- }
- ],
- "styles": [
- "src/styles.scss"
- ],
- "scripts": []
- }
}
}
}
diff --git a/webapp/src/app/core/models/message.model.ts b/webapp/src/app/core/models/message.model.ts
index 89de14b..78ab067 100644
--- a/webapp/src/app/core/models/message.model.ts
+++ b/webapp/src/app/core/models/message.model.ts
@@ -32,4 +32,5 @@ export interface MessageListResponse {
messages: Message[];
next_page_token: string;
page_size: number;
+ total_count: number;
}
diff --git a/webapp/src/app/core/models/sender-name.model.ts b/webapp/src/app/core/models/sender-name.model.ts
index 5b7ede8..6307781 100644
--- a/webapp/src/app/core/models/sender-name.model.ts
+++ b/webapp/src/app/core/models/sender-name.model.ts
@@ -1,9 +1,10 @@
export interface SenderNameStatistics {
name: string;
+ first_timestamp: string;
last_timestamp: string;
count: number;
}
export interface SenderNameListResponse {
- senders: SenderNameStatistics[];
+ sender_names: SenderNameStatistics[];
}
diff --git a/webapp/src/app/features/auth/login/login.component.html b/webapp/src/app/features/auth/login/login.component.html
index 7468219..b2c73a3 100644
--- a/webapp/src/app/features/auth/login/login.component.html
+++ b/webapp/src/app/features/auth/login/login.component.html
@@ -1,7 +1,7 @@
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 379b0e8..d0a985f 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
@@ -92,26 +92,22 @@
nzTooltipTitle="Copy"
[appCopyToClipboard]="channel()!.subscribe_key!"
>
-
+
+
+
Scan this QR code with the SimpleCloudNotifier app to subscribe to this channel.
+
}
@@ -238,15 +234,3 @@
-
-
-
-
- Scan this QR code with the SimpleCloudNotifier app to subscribe to this channel.
-
-
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 db9dcc0..e22c986 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
@@ -44,9 +44,28 @@
}
}
+.qr-section {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid #f0f0f0;
+
+ label {
+ display: block;
+ font-weight: 500;
+ margin-bottom: 12px;
+ color: #333;
+ }
+
+ app-qr-code-display {
+ display: flex;
+ justify-content: center;
+ }
+}
+
.qr-hint {
text-align: center;
color: #666;
font-size: 13px;
- margin-top: 16px;
+ margin-top: 12px;
+ margin-bottom: 0;
}
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 21d57a5..0989501 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
@@ -1,4 +1,4 @@
-import { Component, inject, signal, OnInit } from '@angular/core';
+import { Component, inject, signal, computed, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
@@ -70,9 +70,20 @@ export class ChannelDetailComponent implements OnInit {
editDescription = '';
saving = signal(false);
- // QR modal
- showQrModal = signal(false);
- qrCodeData = signal('');
+ // 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');
@@ -211,28 +222,6 @@ export class ChannelDetailComponent implements OnInit {
});
}
- // QR Code
- showQrCode(): void {
- const channel = this.channel();
- if (!channel || !channel.subscribe_key) return;
-
- const qrText = [
- '@scn.channel.subscribe',
- 'v1',
- channel.display_name,
- channel.owner_user_id,
- channel.channel_id,
- channel.subscribe_key
- ].join('\n');
-
- this.qrCodeData.set(qrText);
- this.showQrModal.set(true);
- }
-
- closeQrModal(): void {
- this.showQrModal.set(false);
- }
-
getSubscriptionStatus(): { label: string; color: string } {
const channel = this.channel();
if (!channel) return { label: 'Unknown', color: 'default' };
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 31398fc..46b599c 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
@@ -110,12 +110,14 @@
- @if (nextPageToken()) {
-
-
-
- }
+
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 99868a6..f41ad08 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
@@ -42,8 +42,17 @@
margin-top: 4px;
}
-.load-more {
+.pagination-controls {
display: flex;
justify-content: center;
+ align-items: center;
+ gap: 16px;
padding: 16px 0;
+
+ .page-indicator {
+ font-size: 14px;
+ color: #666;
+ min-width: 80px;
+ text-align: center;
+ }
}
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 e9422bc..89dc2fc 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
@@ -11,6 +11,7 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
+import { NzPaginationModule } from 'ng-zorro-antd/pagination';
import { ApiService } from '../../../core/services/api.service';
import { AuthService } from '../../../core/services/auth.service';
import { Message, MessageListParams } from '../../../core/models';
@@ -31,6 +32,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
NzSpinModule,
NzCardModule,
NzToolTipModule,
+ NzPaginationModule,
RelativeTimePipe,
],
templateUrl: './message-list.component.html',
@@ -43,7 +45,11 @@ export class MessageListComponent implements OnInit {
messages = signal([]);
loading = signal(false);
- nextPageToken = signal(null);
+
+ // Pagination
+ currentPage = signal(1);
+ pageSize = 50;
+ totalCount = signal(0);
// Filters
searchText = '';
@@ -80,11 +86,11 @@ export class MessageListComponent implements OnInit {
});
}
- loadMessages(append = false): void {
+ loadMessages(): void {
this.loading.set(true);
const params: MessageListParams = {
- page_size: 50,
+ page_size: this.pageSize,
trimmed: true,
};
@@ -97,22 +103,17 @@ export class MessageListComponent implements OnInit {
if (this.channelFilter.length > 0) {
params.channel = this.channelFilter.join(',');
}
- if (append && this.nextPageToken()) {
- params.next_page_token = this.nextPageToken()!;
+
+ // Use page-index based pagination: $1 = page 1, $2 = page 2, etc.
+ const page = this.currentPage();
+ if (page > 1) {
+ params.next_page_token = `$${page}`;
}
this.apiService.getMessages(params).subscribe({
next: (response) => {
- if (append) {
- this.messages.update(msgs => [...msgs, ...response.messages]);
- } else {
- this.messages.set(response.messages);
- }
- this.nextPageToken.set(
- response.next_page_token && response.next_page_token !== '@end'
- ? response.next_page_token
- : null
- );
+ this.messages.set(response.messages);
+ this.totalCount.set(response.total_count);
this.loading.set(false);
},
error: () => {
@@ -123,37 +124,44 @@ export class MessageListComponent implements OnInit {
applyFilters(): void {
this.appliedSearchText = this.searchText;
+ this.currentPage.set(1);
this.loadMessages();
}
onPriorityFilterChange(filters: string[] | null): void {
this.priorityFilter = filters ?? [];
+ this.currentPage.set(1);
this.loadMessages();
}
onChannelFilterChange(filters: string[] | null): void {
this.channelFilter = filters ?? [];
+ this.currentPage.set(1);
this.loadMessages();
}
clearSearch(): void {
this.searchText = '';
this.appliedSearchText = '';
+ this.currentPage.set(1);
this.loadMessages();
}
clearChannelFilter(): void {
this.channelFilter = [];
+ this.currentPage.set(1);
this.loadMessages();
}
removeChannelFilter(channel: string): void {
this.channelFilter = this.channelFilter.filter(c => c !== channel);
+ this.currentPage.set(1);
this.loadMessages();
}
clearPriorityFilter(): void {
this.priorityFilter = [];
+ this.currentPage.set(1);
this.loadMessages();
}
@@ -162,6 +170,7 @@ export class MessageListComponent implements OnInit {
this.appliedSearchText = '';
this.channelFilter = [];
this.priorityFilter = [];
+ this.currentPage.set(1);
this.loadMessages();
}
@@ -175,10 +184,9 @@ export class MessageListComponent implements OnInit {
return channel?.text?.toString() ?? internalName;
}
- loadMore(): void {
- if (this.nextPageToken()) {
- this.loadMessages(true);
- }
+ goToPage(page: number): void {
+ this.currentPage.set(page);
+ this.loadMessages();
}
viewMessage(message: Message): void {
diff --git a/webapp/src/app/features/senders/sender-list/sender-list.component.html b/webapp/src/app/features/senders/sender-list/sender-list.component.html
index cf8bdcc..cb22f6b 100644
--- a/webapp/src/app/features/senders/sender-list/sender-list.component.html
+++ b/webapp/src/app/features/senders/sender-list/sender-list.component.html
@@ -1,50 +1,95 @@
-
-
-
-
- | Sender Name |
- Message Count |
- Last Used |
-
-
-
- @for (sender of senders(); track sender.name) {
-
- |
- {{ sender.name || '(No name)' }}
- |
- {{ sender.count }} |
-
-
- {{ sender.last_timestamp | relativeTime }}
-
- |
-
- } @empty {
-
- |
-
- |
-
- }
-
-
+
+
+
+
+
+
+ | Sender Name |
+ Message Count |
+ Last Used |
+
+
+
+ @for (sender of mySenders(); track sender.name) {
+
+ |
+ {{ sender.name || '(No name)' }}
+ |
+ {{ sender.count }} |
+
+
+ {{ sender.last_timestamp | relativeTime }}
+
+ |
+
+ } @empty {
+
+ |
+
+ |
+
+ }
+
+
+
+
+
+
+
+
+
+ | Sender Name |
+ Message Count |
+ Last Used |
+
+
+
+ @for (sender of allSenders(); track sender.name) {
+
+ |
+ {{ sender.name || '(No name)' }}
+ |
+ {{ sender.count }} |
+
+
+ {{ sender.last_timestamp | relativeTime }}
+
+ |
+
+ } @empty {
+
+ |
+
+ |
+
+ }
+
+
+
+
diff --git a/webapp/src/app/features/senders/sender-list/sender-list.component.ts b/webapp/src/app/features/senders/sender-list/sender-list.component.ts
index 6e1a3e0..a337c50 100644
--- a/webapp/src/app/features/senders/sender-list/sender-list.component.ts
+++ b/webapp/src/app/features/senders/sender-list/sender-list.component.ts
@@ -6,7 +6,9 @@ import { NzIconModule } from 'ng-zorro-antd/icon';
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 { ApiService } from '../../../core/services/api.service';
+import { AuthService } from '../../../core/services/auth.service';
import { SenderNameStatistics } from '../../../core/models';
import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
@@ -21,6 +23,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
NzEmptyModule,
NzCardModule,
NzToolTipModule,
+ NzTabsModule,
RelativeTimePipe,
],
templateUrl: './sender-list.component.html',
@@ -28,24 +31,61 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe';
})
export class SenderListComponent implements OnInit {
private apiService = inject(ApiService);
+ private authService = inject(AuthService);
- senders = signal([]);
- loading = signal(false);
+ mySenders = signal([]);
+ allSenders = signal([]);
+ loadingMy = signal(false);
+ loadingAll = signal(false);
+ activeTab = signal(0);
ngOnInit(): void {
- this.loadSenders();
+ this.loadMySenders();
}
- loadSenders(): void {
- this.loading.set(true);
- this.apiService.getSenderNames().subscribe({
+ onTabChange(index: number): void {
+ this.activeTab.set(index);
+ if (index === 0 && this.mySenders().length === 0) {
+ this.loadMySenders();
+ } else if (index === 1 && this.allSenders().length === 0) {
+ this.loadAllSenders();
+ }
+ }
+
+ loadMySenders(): void {
+ const userId = this.authService.getUserId();
+ if (!userId) return;
+
+ this.loadingMy.set(true);
+ this.apiService.getUserSenderNames(userId).subscribe({
next: (response) => {
- this.senders.set(response.senders);
- this.loading.set(false);
+ this.mySenders.set(response.sender_names);
+ this.loadingMy.set(false);
},
error: () => {
- this.loading.set(false);
+ this.loadingMy.set(false);
}
});
}
+
+ loadAllSenders(): void {
+ this.loadingAll.set(true);
+ this.apiService.getSenderNames().subscribe({
+ next: (response) => {
+ this.allSenders.set(response.sender_names);
+ this.loadingAll.set(false);
+ },
+ error: () => {
+ this.loadingAll.set(false);
+ }
+ });
+ }
+
+ refresh(): void {
+ if (this.activeTab() === 0) {
+ this.loadMySenders();
+ } else {
+ this.loadAllSenders();
+ }
+ }
}
diff --git a/webapp/src/app/layout/main-layout/main-layout.component.html b/webapp/src/app/layout/main-layout/main-layout.component.html
index 2d69632..319ff13 100644
--- a/webapp/src/app/layout/main-layout/main-layout.component.html
+++ b/webapp/src/app/layout/main-layout/main-layout.component.html
@@ -9,10 +9,9 @@
[nzCollapsedWidth]="80"
>
@@ -62,7 +61,6 @@
Logout
-
diff --git a/webapp/src/app/layout/main-layout/main-layout.component.scss b/webapp/src/app/layout/main-layout/main-layout.component.scss
index fca0aef..d3d18d8 100644
--- a/webapp/src/app/layout/main-layout/main-layout.component.scss
+++ b/webapp/src/app/layout/main-layout/main-layout.component.scss
@@ -18,6 +18,7 @@
display: flex;
align-items: center;
justify-content: center;
+ gap: 10px;
padding: 0 16px;
background: #001529;
color: #fff;
@@ -25,6 +26,12 @@
font-weight: 600;
overflow: hidden;
white-space: nowrap;
+
+ .sidebar-logo-img {
+ height: 32px;
+ width: auto;
+ flex-shrink: 0;
+ }
}
.app-header {
@@ -64,11 +71,6 @@
color: #666;
font-size: 13px;
}
-
- .header-logo {
- height: 36px;
- width: auto;
- }
}
.content-area {
diff --git a/webapp/public/favicon.ico b/webapp/src/assets/favicon.ico
similarity index 100%
rename from webapp/public/favicon.ico
rename to webapp/src/assets/favicon.ico