diff --git a/client/angular.json b/client/angular.json
index 7d7e545..48515e1 100644
--- a/client/angular.json
+++ b/client/angular.json
@@ -53,6 +53,9 @@
],
"styles": [
"src/styles.scss"
+ ],
+ "scripts": [
+ "src/jsonview.js"
]
},
"configurations": {
diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts
index 6b19fd8..399fb4c 100644
--- a/client/src/app/app.routes.ts
+++ b/client/src/app/app.routes.ts
@@ -9,6 +9,10 @@ export const routes: Routes = [
path: '',
component: HomePageComponent,
},
+ {
+ path: 'chat',
+ component: ChatPageComponent,
+ },
{
path: 'chat/:peerId',
component: ChatPageComponent,
diff --git a/client/src/app/chat-page.component.html b/client/src/app/chat-page.component.html
index 972026a..73591cd 100644
--- a/client/src/app/chat-page.component.html
+++ b/client/src/app/chat-page.component.html
@@ -42,35 +42,45 @@
}
@for (connectedPeer of session.peers(); track connectedPeer.id) {
-
+
+
+
+
}
@@ -121,6 +131,10 @@
/>
}
+ @if (isIncomingJsonFileEntry(entry)) {
+
+ }
+
{{ entry.fileName }}
@if (entry.fileSize) {
diff --git a/client/src/app/chat-page.component.scss b/client/src/app/chat-page.component.scss
index 1e5e5cd..6bf832a 100644
--- a/client/src/app/chat-page.component.scss
+++ b/client/src/app/chat-page.component.scss
@@ -106,8 +106,12 @@
}
.peer-tile {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.75rem;
+ align-items: center;
width: 100%;
- padding: 0.95rem 1rem;
+ padding: 0.8rem 0.85rem 0.8rem 1rem;
border: 1px solid var(--surface-border);
border-radius: 1rem;
color: inherit;
@@ -115,6 +119,30 @@
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
}
+.peer-tile-main {
+ min-width: 0;
+ padding: 0;
+ border: 0;
+ color: inherit;
+ background: transparent;
+}
+
+.peer-tile-delete {
+ width: 2.2rem;
+ height: 2.2rem;
+ padding: 0;
+ border: 0;
+ border-radius: 999px;
+ background: transparent;
+ font-size: 1rem;
+ line-height: 1;
+}
+
+.peer-tile-delete:hover,
+.peer-tile-delete:focus-visible {
+ background: var(--badge-background);
+}
+
.peer-tile:hover,
.peer-tile:focus-visible,
.peer-tile-active {
diff --git a/client/src/app/chat-page.component.ts b/client/src/app/chat-page.component.ts
index f693ea7..891c47a 100644
--- a/client/src/app/chat-page.component.ts
+++ b/client/src/app/chat-page.component.ts
@@ -5,11 +5,12 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ChatSessionService } from './chat-session.service';
+import { JsonFileViewerComponent } from './json-file-viewer.component';
import type { ChatEntry, ConnectionState } from './models';
@Component({
selector: 'app-chat-page',
- imports: [CommonModule, FormsModule, RouterLink],
+ imports: [CommonModule, FormsModule, RouterLink, JsonFileViewerComponent],
templateUrl: './chat-page.component.html',
styleUrl: './chat-page.component.scss',
})
@@ -120,10 +121,25 @@ export class ChatPageComponent {
await this.session.deleteMessage(entry);
}
+ async deleteConversation(peerId: string, event?: Event): Promise
{
+ event?.stopPropagation();
+ await this.session.deleteConversation(peerId);
+ }
+
isImageEntry(entry: ChatEntry): boolean {
return entry.kind === 'file' && !!entry.downloadUrl && (entry.fileMimeType?.startsWith('image/') ?? false);
}
+ isIncomingJsonFileEntry(entry: ChatEntry): boolean {
+ return (
+ entry.kind === 'file' &&
+ entry.direction === 'incoming' &&
+ !!entry.downloadUrl &&
+ !!entry.fileName &&
+ entry.fileName.toLowerCase().endsWith('.json')
+ );
+ }
+
isPeerTyping(peerId: string): boolean {
return this.session.typingPeerIds().includes(peerId);
}
diff --git a/client/src/app/chat-session.service.ts b/client/src/app/chat-session.service.ts
index a8a45b8..4068cea 100644
--- a/client/src/app/chat-session.service.ts
+++ b/client/src/app/chat-session.service.ts
@@ -1060,6 +1060,51 @@ export class ChatSessionService {
}
}
+ async deleteConversation(peerId: string): Promise {
+ const entries = this.messages().filter((entry) => entry.peerId === peerId);
+
+ for (const entry of entries) {
+ this.removeMessageById(entry.id);
+ }
+
+ this.clearUnreadPeer(peerId);
+ this.clearPeerTyping(peerId);
+
+ const currentUserId = this.currentUser()?.id;
+
+ if (!currentUserId) {
+ return;
+ }
+
+ try {
+ const conversationKey = this.conversationStorageKey(currentUserId, peerId);
+ await this.queueMessageStoreOperation(conversationKey, async () => {
+ const database = await this.openMessageDatabase();
+
+ if (!database) {
+ return;
+ }
+
+ const transaction = database.transaction(ChatSessionService.messageStoreName, 'readwrite');
+ const store = transaction.objectStore(ChatSessionService.messageStoreName);
+ const conversationIndex = store.index('conversationKeyCreatedAt');
+ const rows = await this.waitForRequest(
+ conversationIndex.getAll(
+ IDBKeyRange.bound([conversationKey, 0], [conversationKey, Number.MAX_SAFE_INTEGER]),
+ ),
+ ) as PersistedChatEntry[];
+
+ for (const row of rows) {
+ store.delete(row.storageKey);
+ }
+
+ await this.waitForTransaction(transaction);
+ });
+ } catch (error) {
+ console.warn('Could not delete chat conversation.', error);
+ }
+ }
+
private addSystemMessage(peerId: string, text: string): void {
const id = crypto.randomUUID();
diff --git a/client/src/app/home-page.component.html b/client/src/app/home-page.component.html
index 6254448..994b3a8 100644
--- a/client/src/app/home-page.component.html
+++ b/client/src/app/home-page.component.html
@@ -25,7 +25,7 @@
{{ user.displayName }}
{{ user.username }}
{{ session.status() }}
-