From e973d850ed6e752da9c677dd3b88014accf4303c Mon Sep 17 00:00:00 2001 From: diyaa Date: Sun, 26 Jul 2026 21:43:39 +0200 Subject: [PATCH] version 1.0 --- .../FchatiApp/Features/Chat/ChatView.swift | 2 ++ .../Features/Settings/SettingsView.swift | 6 +++++ .../FchatiApp/Session/AppSession.swift | 19 ++++++++++++++ IMPLEMENTATION_STATUS.md | 26 ++++++++++++++++++- relay-server/src/index.js | 7 +++++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift b/FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift index 4f7a609..aab3b5e 100644 --- a/FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift +++ b/FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift @@ -32,9 +32,11 @@ struct ChatView: View { } .onAppear { scrollToLatest(using: proxy) + Task { await session.markVisibleMessagesRead() } } .onChange(of: session.messages.last?.id) { _, _ in scrollToLatest(using: proxy) + Task { await session.markVisibleMessagesRead() } } } diff --git a/FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift b/FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift index 4d7b5c0..e18af61 100644 --- a/FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift +++ b/FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift @@ -53,6 +53,12 @@ struct SettingsView: View { Text(appVersion) } } + + Section { + Button("Quit Fchati", role: .destructive) { + NSApplication.shared.terminate(nil) + } + } } .formStyle(.grouped) .padding() diff --git a/FchatiApp/Sources/FchatiApp/Session/AppSession.swift b/FchatiApp/Sources/FchatiApp/Session/AppSession.swift index c43c9dc..cdcd5b5 100644 --- a/FchatiApp/Sources/FchatiApp/Session/AppSession.swift +++ b/FchatiApp/Sources/FchatiApp/Session/AppSession.swift @@ -101,6 +101,17 @@ final class AppSession: ObservableObject { try await sendAndStore(message) } + func markVisibleMessagesRead() async { + let unread = messages.filter { !$0.isRead && $0.from != KeychainStore.installationID } + guard !unread.isEmpty else { return } + + let ids = unread.map { $0.id } + ids.forEach { markMessageAsRead(id: $0) } + + let receipt = WSMessage(type: "read", messageIDs: ids) + try? await webSocket.send(receipt) + } + func unpair() { incomingMessagesTask?.cancel() incomingMessagesTask = nil @@ -192,6 +203,14 @@ final class AppSession: ObservableObject { await storeIncomingMessage(chatMessage) NotificationManager.shared.notify(from: chatMessage.fromName, body: chatMessage.body) + case "peer.joined": + // The partner just came online — save their identity and transition to connected. + if let peerID = message.peerID, let peerName = message.fromName { + KeychainStore.peerID = peerID + KeychainStore.peerName = peerName + state = .connected(peerID: peerID, peerName: peerName) + } + case "read": for id in message.messageIDs ?? [] { markMessageAsRead(id: id) diff --git a/IMPLEMENTATION_STATUS.md b/IMPLEMENTATION_STATUS.md index 9f1b324..48415c4 100644 --- a/IMPLEMENTATION_STATUS.md +++ b/IMPLEMENTATION_STATUS.md @@ -225,6 +225,30 @@ --- +## Bug Fixes — Full Code Audit ✅ + +**Applied by:** Claude + +### Fix 3 — Creator never reaches `.connected` state (critical) + +**Files:** +- `relay-server/src/index.js` — after WebSocket auth, notify the partner with `{ type: "peer.joined", peerID, peerName }` +- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — handle `peer.joined`: save peerID/peerName to Keychain, transition state to `.connected` + +**Problem:** When the creator generates a pairing code and connects via WebSocket, their state gets stuck at `.connecting` forever. The server never told the creator that the joiner joined — it only responded to the joiner's HTTP request. The creator had no way to learn the joiner's identity or transition to `.connected`. + +**Fix:** Server now sends `{ type: "peer.joined", peerID, peerName }` to the online partner whenever any peer authenticates on WebSocket. Client handles this event and saves credentials + transitions state. + +--- + +### Fix 4 — Read receipts were never sent (important) + +**Files:** +- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — added `markVisibleMessagesRead()`: marks all incoming unread messages as read and sends `{ type: "read", messageIDs: [...] }` via WebSocket +- `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` — calls `markVisibleMessagesRead()` on appear and when new messages arrive + +**Problem:** The code handled *incoming* read receipts correctly but never *sent* them. The partner's messages were never acknowledged as read, so the partner never saw "read" status on their messages. + ## Pending -Nothing remaining in the codebase. Next step: deploy relay server to production. +Nothing remaining. diff --git a/relay-server/src/index.js b/relay-server/src/index.js index e097182..95ee831 100644 --- a/relay-server/src/index.js +++ b/relay-server/src/index.js @@ -417,6 +417,13 @@ wss.on('connection', (ws) => { send(ws, { type: 'auth.ok', peerID }); console.log(`[ws] connected: ${peerName} (${peerID})`); + // Notify the partner that this peer is now online + const partnerIDOnAuth = peerPartnerID(session, peerID); + const partnerConnOnAuth = connections.get(partnerIDOnAuth); + if (partnerConnOnAuth?.ws.readyState === 1) { + send(partnerConnOnAuth.ws, { type: 'peer.joined', peerID, peerName }); + } + // Deliver messages that arrived while this peer was offline flushQueue(peerID, ws); return;