Sorry Change name to Fchaty

This commit is contained in:
diyaa 2026-07-27 12:09:51 +02:00
parent 2e1d40caad
commit b32911dfb2
32 changed files with 172 additions and 142 deletions

View File

@ -1,18 +0,0 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "FchatiApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "FchatiApp",
path: "Sources/FchatiApp"
),
.testTarget(
name: "FchatiAppTests",
dependencies: ["FchatiApp"],
path: "Tests/FchatiAppTests"
)
]
)

View File

@ -10,5 +10,7 @@
<true/> <true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -5,15 +5,15 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>FchatiApp</string> <string>FchatyApp</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>de.diyaa.fchati</string> <string>de.diyaa.fchaty</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Fchati.icns</string> <string>Fchaty.icns</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Fchati</string> <string>Fchaty</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@ -25,6 +25,6 @@
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Fchati uses the microphone to record voice messages.</string> <string>Fchaty uses the microphone to record voice messages.</string>
</dict> </dict>
</plist> </plist>

18
FchatyApp/Package.swift Normal file
View File

@ -0,0 +1,18 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "FchatyApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "FchatyApp",
path: "Sources/FchatyApp"
),
.testTarget(
name: "FchatyAppTests",
dependencies: ["FchatyApp"],
path: "Tests/FchatyAppTests"
)
]
)

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -3,9 +3,9 @@ import AppKit
import SwiftUI import SwiftUI
@main @main
struct FchatiApp: App { struct FchatyApp: App {
var body: some Scene { var body: some Scene {
WindowGroup("Fchati") { WindowGroup("Fchaty") {
RootView() RootView()
} }
.defaultSize(width: 420, height: 560) .defaultSize(width: 420, height: 560)
@ -79,8 +79,8 @@ private struct RootView: View {
} label: { } label: {
Image(systemName: "power") Image(systemName: "power")
} }
.accessibilityLabel("Quit Fchati") .accessibilityLabel("Quit Fchaty")
.help("Quit Fchati") .help("Quit Fchaty")
} }
.padding() .padding()

View File

@ -16,6 +16,19 @@ struct ChatView: View {
} }
var body: some View { var body: some View {
switch session.state {
case .unpaired:
notConnectedPlaceholder(icon: "person.2.slash", message: "Pair with someone to start chatting.")
case .connecting:
notConnectedPlaceholder(icon: "antenna.radiowaves.left.and.right", message: "Connecting...")
case .error(let msg):
notConnectedPlaceholder(icon: "exclamationmark.triangle", message: msg)
case .connected:
chatContent
}
}
private var chatContent: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
ScrollViewReader { proxy in ScrollViewReader { proxy in
ScrollView { ScrollView {
@ -61,9 +74,7 @@ struct ChatView: View {
TextField("Message", text: $viewModel.draft) TextField("Message", text: $viewModel.draft)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.onSubmit { .onSubmit {
Task { Task { await viewModel.sendText() }
await viewModel.sendText()
}
} }
Image(systemName: viewModel.isRecording ? "mic.fill" : "mic") Image(systemName: viewModel.isRecording ? "mic.fill" : "mic")
@ -72,19 +83,13 @@ struct ChatView: View {
.contentShape(Circle()) .contentShape(Circle())
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in viewModel.startRecording() }
viewModel.startRecording() .onEnded { _ in viewModel.stopRecordingAndSend() }
}
.onEnded { _ in
viewModel.stopRecordingAndSend()
}
) )
.help("Hold to record audio") .help("Hold to record audio")
Button("Send") { Button("Send") {
Task { Task { await viewModel.sendText() }
await viewModel.sendText()
}
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled( .disabled(
@ -97,6 +102,20 @@ struct ChatView: View {
} }
} }
private func notConnectedPlaceholder(icon: String, message: String) -> some View {
VStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundStyle(.secondary)
Text(message)
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
private func chooseFile() { private func chooseFile() {
let panel = NSOpenPanel() let panel = NSOpenPanel()
panel.canChooseDirectories = false panel.canChooseDirectories = false

View File

@ -58,6 +58,19 @@ final class ChatViewModel: ObservableObject {
return return
} }
// Request microphone permission before attempting to record.
AVCaptureDevice.requestAccess(for: .audio) { granted in
DispatchQueue.main.async {
if granted {
self.beginRecording()
} else {
self.errorMessage = "Microphone access denied. Enable it in System Settings."
}
}
}
}
private func beginRecording() {
do { do {
let url = try makeRecordingURL() let url = try makeRecordingURL()
let settings: [String: Any] = [ let settings: [String: Any] = [
@ -83,6 +96,7 @@ final class ChatViewModel: ObservableObject {
} }
} }
func stopRecordingAndSend() { func stopRecordingAndSend() {
guard isRecording, let recorder, let recordingURL else { guard isRecording, let recorder, let recordingURL else {
return return
@ -106,7 +120,7 @@ final class ChatViewModel: ObservableObject {
create: true create: true
) )
let recordingsDirectory = cachesDirectory.appendingPathComponent( let recordingsDirectory = cachesDirectory.appendingPathComponent(
"FchatiRecordings", "FchatyRecordings",
isDirectory: true isDirectory: true
) )

View File

@ -57,9 +57,6 @@ struct PairingView: View {
Text("Pair a new device") Text("Pair a new device")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
TextField("Your display name", text: $viewModel.displayName)
.textFieldStyle(.roundedBorder)
if let generatedCode = viewModel.generatedCode { if let generatedCode = viewModel.generatedCode {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Share this code") Text("Share this code")
@ -109,9 +106,6 @@ struct PairingView: View {
Text("Join a pairing") Text("Join a pairing")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
TextField("Your display name", text: $viewModel.displayName)
.textFieldStyle(.roundedBorder)
TextField("Pairing code", text: $viewModel.code) TextField("Pairing code", text: $viewModel.code)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)

View File

@ -1,5 +1,6 @@
import Combine import Combine
import Foundation import Foundation
import AppKit
enum PairingMode: String, CaseIterable, Identifiable { enum PairingMode: String, CaseIterable, Identifiable {
case create = "Create Code" case create = "Create Code"
@ -17,7 +18,7 @@ final class PairingViewModel: ObservableObject {
isWaitingForPeer = false isWaitingForPeer = false
} }
} }
@Published var displayName = "" @Published var displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
@Published var code = "" { @Published var code = "" {
didSet { didSet {
let uppercasedCode = code.uppercased() let uppercasedCode = code.uppercased()

View File

@ -55,7 +55,7 @@ struct SettingsView: View {
} }
Section { Section {
Button("Quit Fchati", role: .destructive) { Button("Quit Fchaty", role: .destructive) {
NSApplication.shared.terminate(nil) NSApplication.shared.terminate(nil)
} }
} }

View File

@ -209,7 +209,7 @@ final class AppSession: ObservableObject {
KeychainStore.peerID = peerID KeychainStore.peerID = peerID
KeychainStore.peerName = peerName KeychainStore.peerName = peerName
state = .connected(peerID: peerID, peerName: peerName) state = .connected(peerID: peerID, peerName: peerName)
NotificationManager.shared.notify(from: "Fchati", body: "\(peerName) connected") NotificationManager.shared.notify(from: "Fchaty", body: "\(peerName) connected")
} }
case "read": case "read":

View File

@ -33,7 +33,7 @@ enum KeychainStore {
delete(.peerName) delete(.peerName)
} }
private static let service = "de.diyaa.fchati" private static let service = "de.diyaa.fchaty"
private enum Key: String { private enum Key: String {
case installationID case installationID

View File

@ -82,7 +82,7 @@ actor MessageStore {
create: true create: true
) )
let directory = applicationSupportDirectory.appendingPathComponent( let directory = applicationSupportDirectory.appendingPathComponent(
"de.diyaa.fchati", "de.diyaa.fchaty",
isDirectory: true isDirectory: true
) )

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class AppTabTests: XCTestCase { final class AppTabTests: XCTestCase {
func testTabsMatchTheAppShell() { func testTabsMatchTheAppShell() {

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class ChatMessageBubbleTests: XCTestCase { final class ChatMessageBubbleTests: XCTestCase {
func testMessageKeepsMarkdownBodyForRendering() { func testMessageKeepsMarkdownBodyForRendering() {

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class MessageStoreModelTests: XCTestCase { final class MessageStoreModelTests: XCTestCase {
func testMessageRoundTripPreservesAttachmentAndReadStatus() throws { func testMessageRoundTripPreservesAttachmentAndReadStatus() throws {

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class PairingViewModelTests: XCTestCase { final class PairingViewModelTests: XCTestCase {
func testPairingCodeIsUppercasedAndPrefixed() { func testPairingCodeIsUppercasedAndPrefixed() {

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class RelayAPIResponseTests: XCTestCase { final class RelayAPIResponseTests: XCTestCase {
func testPairingCreateResponseDecodesServerPayload() throws { func testPairingCreateResponseDecodesServerPayload() throws {

View File

@ -1,5 +1,5 @@
import XCTest import XCTest
@testable import FchatiApp @testable import FchatyApp
final class WSMessageTests: XCTestCase { final class WSMessageTests: XCTestCase {
func testMessageRoundTripPreservesServerFields() throws { func testMessageRoundTripPreservesServerFields() throws {

View File

@ -7,18 +7,18 @@
### What was built ### What was built
- `FchatiApp/Package.swift` — SPM package, macOS 14+, no Xcode - `FchatyApp/Package.swift` — SPM package, macOS 14+, no Xcode
- `FchatiApp/Sources/FchatiApp/FchatiApp.swift` — MenuBarExtra (.window style), 360pt wide, 480pt min height, three tabs (Chat / Pairing / Settings) with segmented picker - `FchatyApp/Sources/FchatyApp/FchatyApp.swift` — MenuBarExtra (.window style), 360pt wide, 480pt min height, three tabs (Chat / Pairing / Settings) with segmented picker
- `FchatiApp/FchatiApp.entitlements` — sandbox + outgoing/incoming network - `FchatyApp/FchatyApp.entitlements` — sandbox + outgoing/incoming network
- `FchatiApp/Info.plist` — bundle ID `de.diyaa.fchati`, version 0.1.0, `LSUIElement = YES` (hides Dock icon) - `FchatyApp/Info.plist` — bundle ID `de.diyaa.fchaty`, version 0.1.0, `LSUIElement = YES` (hides Dock icon)
- `FchatiApp/Tests/FchatiAppTests/AppTabTests.swift` — verifies all three tabs and their icons exist - `FchatyApp/Tests/FchatyAppTests/AppTabTests.swift` — verifies all three tabs and their icons exist
- `scripts/run-macos-app.sh` — clean → test → release build → bundle → codesign → install to /Applications → open - `scripts/run-macos-app.sh` — clean → test → release build → bundle → codesign → install to /Applications → open
### Verified ### Verified
- `swift build` passes (debug) - `swift build` passes (debug)
- `swift test` passes (1 test) - `swift test` passes (1 test)
- Script installs and opens `/Applications/Fchati.app` successfully - Script installs and opens `/Applications/Fchaty.app` successfully
### Fixes applied after agent review ### Fixes applied after agent review
@ -33,14 +33,14 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Storage/KeychainStore.swift` — direct Security.framework wrapper for the installation ID, auth token, peer ID, and peer name. - `FchatyApp/Sources/FchatyApp/Storage/KeychainStore.swift` — direct Security.framework wrapper for the installation ID, auth token, peer ID, and peer name.
- The installation ID is created once as a UUID and persisted in Keychain. - The installation ID is created once as a UUID and persisted in Keychain.
- Token and peer properties support read, update, and deletion through Swift property syntax. - Token and peer properties support read, update, and deletion through Swift property syntax.
- `clearAll()` removes pairing credentials while preserving the installation ID. - `clearAll()` removes pairing credentials while preserving the installation ID.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
## TASK-03 — RelayAPI (HTTP) ✅ ## TASK-03 — RelayAPI (HTTP) ✅
@ -48,15 +48,15 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Networking/RelayAPI.swift` — actor-based HTTP client for pairing creation, pairing join, file upload, and file download. - `FchatyApp/Sources/FchatyApp/Networking/RelayAPI.swift` — actor-based HTTP client for pairing creation, pairing join, file upload, and file download.
- Pairing requests use JSON and decode the exact relay response payloads. - Pairing requests use JSON and decode the exact relay response payloads.
- File uploads use `multipart/form-data` with the required `file` field and bearer-token authorization. - File uploads use `multipart/form-data` with the required `file` field and bearer-token authorization.
- Non-success HTTP responses, transport failures, and response decoding failures map to `RelayAPIError`. - Non-success HTTP responses, transport failures, and response decoding failures map to `RelayAPIError`.
- `FchatiApp/Tests/FchatiAppTests/RelayAPIResponseTests.swift` — response-decoding coverage for relay pairing and upload payloads. - `FchatyApp/Tests/FchatyAppTests/RelayAPIResponseTests.swift` — response-decoding coverage for relay pairing and upload payloads.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with the relay response tests. - `swift test` passes with the relay response tests.
## TASK-04 — WSClient (WebSocket) ✅ ## TASK-04 — WSClient (WebSocket) ✅
@ -65,15 +65,15 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Networking/WSClient.swift` — actor-based WebSocket client using `URLSessionWebSocketTask`. - `FchatyApp/Sources/FchatyApp/Networking/WSClient.swift` — actor-based WebSocket client using `URLSessionWebSocketTask`.
- Connect sends authentication immediately, requires an `auth.ok` response within 10 seconds, and exposes authenticated incoming messages through `AsyncStream`. - Connect sends authentication immediately, requires an `auth.ok` response within 10 seconds, and exposes authenticated incoming messages through `AsyncStream`.
- The client sends a ping every 30 seconds and reconnects after unexpected disconnects with delays of 2, 4, 8, 16, and 32 seconds. - The client sends a ping every 30 seconds and reconnects after unexpected disconnects with delays of 2, 4, 8, 16, and 32 seconds.
- Explicit disconnects cancel reconnect attempts and retain the incoming stream for a later connection. - Explicit disconnects cancel reconnect attempts and retain the incoming stream for a later connection.
- `FchatiApp/Tests/FchatiAppTests/WSMessageTests.swift` — serialization coverage for relay WebSocket messages. - `FchatyApp/Tests/FchatyAppTests/WSMessageTests.swift` — serialization coverage for relay WebSocket messages.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with the WebSocket message test. - `swift test` passes with the WebSocket message test.
## TASK-05 — AppSession (State Machine) ✅ ## TASK-05 — AppSession (State Machine) ✅
@ -82,7 +82,7 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — main-actor application state for pairing, session restoration, message sending, file sending, read receipts, and incoming message handling. - `FchatyApp/Sources/FchatyApp/Session/AppSession.swift` — main-actor application state for pairing, session restoration, message sending, file sending, read receipts, and incoming message handling.
- Existing credentials restore the WebSocket connection at launch. - Existing credentials restore the WebSocket connection at launch.
- Pairing and message failures update the published state with an error description. - Pairing and message failures update the published state with an error description.
- File uploads are converted to attachment metadata and forwarded through relay chat messages. - File uploads are converted to attachment metadata and forwarded through relay chat messages.
@ -93,9 +93,9 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift` — actor-backed JSON persistence at the required Application Support location. - `FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift` — actor-backed JSON persistence at the required Application Support location.
- Message writes are atomic, deduplicated by message ID, sorted by timestamp, and retain read-receipt state. - Message writes are atomic, deduplicated by message ID, sorted by timestamp, and retain read-receipt state.
- `FchatiApp/Tests/FchatiAppTests/MessageStoreModelTests.swift` — serialization coverage for messages and attachment metadata. - `FchatyApp/Tests/FchatyAppTests/MessageStoreModelTests.swift` — serialization coverage for messages and attachment metadata.
## TASK-10 — New Message Notifications ✅ ## TASK-10 — New Message Notifications ✅
@ -103,12 +103,12 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift` — notification permission request and background message notification delivery. - `FchatyApp/Sources/FchatyApp/Notifications/NotificationManager.swift` — notification permission request and background message notification delivery.
- Notifications use the sender name, limit the preview to 100 characters, and are suppressed while the app is active. - Notifications use the sender name, limit the preview to 100 characters, and are suppressed while the app is active.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with session storage coverage. - `swift test` passes with session storage coverage.
## TASK-06 — PairingView ✅ ## TASK-06 — PairingView ✅
@ -117,15 +117,15 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Features/Pairing/PairingView.swift` — create and join pairing screens with a segmented mode selector, code copy action, loading states, and inline errors. - `FchatyApp/Sources/FchatyApp/Features/Pairing/PairingView.swift` — create and join pairing screens with a segmented mode selector, code copy action, loading states, and inline errors.
- `FchatiApp/Sources/FchatiApp/Features/Pairing/PairingViewModel.swift` — pairing request orchestration, code normalization, and display-state management. - `FchatyApp/Sources/FchatyApp/Features/Pairing/PairingViewModel.swift` — pairing request orchestration, code normalization, and display-state management.
- Join codes are uppercased automatically and accept values with or without the `FCHT-` prefix. - Join codes are uppercased automatically and accept values with or without the `FCHT-` prefix.
- A connection callback allows the app shell to switch to the chat screen when pairing succeeds. - A connection callback allows the app shell to switch to the chat screen when pairing succeeds.
- `FchatiApp/Tests/FchatiAppTests/PairingViewModelTests.swift` — normalization coverage for pairing code input. - `FchatyApp/Tests/FchatyAppTests/PairingViewModelTests.swift` — normalization coverage for pairing code input.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with pairing view-model coverage. - `swift test` passes with pairing view-model coverage.
## TASK-07 — ChatView ✅ ## TASK-07 — ChatView ✅
@ -134,15 +134,15 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` — scrollable chat UI with automatic scrolling, text submission on Return, file selection, and hold-to-record voice input. - `FchatyApp/Sources/FchatyApp/Features/Chat/ChatView.swift` — scrollable chat UI with automatic scrolling, text submission on Return, file selection, and hold-to-record voice input.
- `FchatiApp/Sources/FchatiApp/Features/Chat/ChatViewModel.swift` — message and file sending, `.m4a` recording lifecycle, temporary recording cleanup, and inline send errors. - `FchatyApp/Sources/FchatyApp/Features/Chat/ChatViewModel.swift` — message and file sending, `.m4a` recording lifecycle, temporary recording cleanup, and inline send errors.
- `FchatiApp/Sources/FchatiApp/Features/Chat/MessageBubble.swift` — left and right message bubbles, Markdown body rendering, attachment metadata, and timestamps. - `FchatyApp/Sources/FchatyApp/Features/Chat/MessageBubble.swift` — left and right message bubbles, Markdown body rendering, attachment metadata, and timestamps.
- `FchatiApp/FchatiApp.entitlements` and `FchatiApp/Info.plist` — microphone sandbox entitlement and privacy usage description required for voice recording. - `FchatyApp/FchatyApp.entitlements` and `FchatyApp/Info.plist` — microphone sandbox entitlement and privacy usage description required for voice recording.
- `FchatiApp/Tests/FchatiAppTests/ChatMessageBubbleTests.swift` — Markdown rendering coverage for message bodies. - `FchatyApp/Tests/FchatyAppTests/ChatMessageBubbleTests.swift` — Markdown rendering coverage for message bodies.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with chat view coverage. - `swift test` passes with chat view coverage.
## TASK-09 — SettingsView ✅ ## TASK-09 — SettingsView ✅
@ -151,12 +151,12 @@
### What was built ### What was built
- `FchatiApp/Sources/FchatiApp/Features/Settings/SettingsView.swift` — editable display name, published session connection state, destructive unpair action, and bundled application version display. - `FchatyApp/Sources/FchatyApp/Features/Settings/SettingsView.swift` — editable display name, published session connection state, destructive unpair action, and bundled application version display.
- The view reads and writes the display name through `KeychainStore.peerName` and calls `AppSession.unpair()` to clear pairing credentials. - The view reads and writes the display name through `KeychainStore.peerName` and calls `AppSession.unpair()` to clear pairing credentials.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with all current application tests. - `swift test` passes with all current application tests.
## Integration Update ✅ ## Integration Update ✅
@ -165,13 +165,13 @@
### What was updated ### What was updated
- `FchatiApp/Sources/FchatiApp/FchatiApp.swift` now renders `PairingView`, `ChatView`, and `SettingsView` in the menu-bar tabs instead of placeholder content. - `FchatyApp/Sources/FchatyApp/FchatyApp.swift` now renders `PairingView`, `ChatView`, and `SettingsView` in the menu-bar tabs instead of placeholder content.
- The initial tab is pairing for first-time setup. - The initial tab is pairing for first-time setup.
- A successful pairing switches the selected tab to chat. - A successful pairing switches the selected tab to chat.
### Verified ### Verified
- `swift build` passes from `FchatiApp/`. - `swift build` passes from `FchatyApp/`.
- `swift test` passes with all current application tests. - `swift test` passes with all current application tests.
## Server File Cleanup ✅ ## Server File Cleanup ✅
@ -199,7 +199,7 @@
- `relay-server/Dockerfile` initializes all persistent-data paths as the unprivileged `node` user, preventing pairing, queue, and upload write failures on newly created volumes. - `relay-server/Dockerfile` initializes all persistent-data paths as the unprivileged `node` user, preventing pairing, queue, and upload write failures on newly created volumes.
- The macOS app now has an always-visible quit control beside the top-level tabs, in addition to the existing Settings action. - The macOS app now has an always-visible quit control beside the top-level tabs, in addition to the existing Settings action.
- The macOS app now includes a normal main window and Dock presence while retaining the menu-bar shortcut. - The macOS app now includes a normal main window and Dock presence while retaining the menu-bar shortcut.
- `FchatiApp/Resources/AppIcon.png` and `FchatiApp/Resources/Fchati.icns` provide the bundled Dock and application icon, and the installer copies the icon into the application bundle. - `FchatyApp/Resources/AppIcon.png` and `FchatyApp/Resources/Fchaty.icns` provide the bundled Dock and application icon, and the installer copies the icon into the application bundle.
- The supplied Teamwork Chat icon replaces the app icon; the menu bar uses the native monochrome message symbol for visual consistency with macOS. - The supplied Teamwork Chat icon replaces the app icon; the menu bar uses the native monochrome message symbol for visual consistency with macOS.
## Bug Fixes — Post-Review ✅ ## Bug Fixes — Post-Review ✅
@ -208,7 +208,7 @@
### Fix 1 — `isSent` detection in ChatView ### Fix 1 — `isSent` detection in ChatView
**File:** `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift` **File:** `FchatyApp/Sources/FchatyApp/Features/Chat/ChatView.swift`
**Problem:** `MessageBubble` was deciding which side to render on using `message.fromName == "You"`. If the user ever set their display name to "You" this would misidentify received messages as sent. **Problem:** `MessageBubble` was deciding which side to render on using `message.fromName == "You"`. If the user ever set their display name to "You" this would misidentify received messages as sent.
@ -219,8 +219,8 @@
### Fix 2 — Message timestamps for queued/offline messages ### Fix 2 — Message timestamps for queued/offline messages
**Files:** **Files:**
- `FchatiApp/Sources/FchatiApp/Networking/WSClient.swift` — added `sentAt: String?` field to `WSMessage` - `FchatyApp/Sources/FchatyApp/Networking/WSClient.swift` — added `sentAt: String?` field to `WSMessage`
- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — sender embeds ISO-8601 timestamp; receiver parses it - `FchatyApp/Sources/FchatyApp/Session/AppSession.swift` — sender embeds ISO-8601 timestamp; receiver parses it
**Problem:** When a message was delivered from the offline queue, `sentAt` was set to `Date()` (arrival time). Messages sent at 11pm but delivered after a restart showed a wrong timestamp. **Problem:** When a message was delivered from the offline queue, `sentAt` was set to `Date()` (arrival time). Messages sent at 11pm but delivered after a restart showed a wrong timestamp.
@ -236,7 +236,7 @@
**Files:** **Files:**
- `relay-server/src/index.js` — after WebSocket auth, notify the partner with `{ type: "peer.joined", peerID, peerName }` - `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` - `FchatyApp/Sources/FchatyApp/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`. **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`.
@ -247,8 +247,8 @@
### Fix 4 — Read receipts were never sent (important) ### Fix 4 — Read receipts were never sent (important)
**Files:** **Files:**
- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — added `markVisibleMessagesRead()`: marks all incoming unread messages as read and sends `{ type: "read", messageIDs: [...] }` via WebSocket - `FchatyApp/Sources/FchatyApp/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 - `FchatyApp/Sources/FchatyApp/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. **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.

View File

@ -1,4 +1,4 @@
# Fchati — Current State & Tasks # Fchaty — Current State & Tasks
> **Important:** The Mac app must be built entirely without opening Xcode. > **Important:** The Mac app must be built entirely without opening Xcode.
> Use `Package.swift` (Swift Package Manager) as the project structure. > Use `Package.swift` (Swift Package Manager) as the project structure.
@ -24,7 +24,7 @@ This tells the next agent not to redo your work.
Update `IMPLEMENTATION_STATUS.md` with: what files you created or changed, what was verified (tests, build), and anything you found missing or left incomplete. Write in English. This file is how the next session picks up exactly where you left off. Update `IMPLEMENTATION_STATUS.md` with: what files you created or changed, what was verified (tests, build), and anything you found missing or left incomplete. Write in English. This file is how the next session picks up exactly where you left off.
**4. Verify before finishing.** **4. Verify before finishing.**
Run `swift build` from `FchatiApp/` before declaring the task complete. If it does not compile, fix it. Do not leave broken code. Run `swift build` from `FchatyApp/` before declaring the task complete. If it does not compile, fix it. Do not leave broken code.
**5. Do not touch completed tasks.** **5. Do not touch completed tasks.**
If a task is marked `[x]`, do not modify those files unless explicitly instructed. If a task is marked `[x]`, do not modify those files unless explicitly instructed.
@ -73,7 +73,7 @@ These are decisions made during planning — do not change without revisiting th
### Not built yet ### Not built yet
The Mac app (`FchatiApp/`) does not exist yet. The Mac app (`FchatyApp/`) does not exist yet.
### Pending server tasks ### Pending server tasks
@ -90,11 +90,11 @@ The Mac app (`FchatiApp/`) does not exist yet.
``` ```
f-chaty-native-new/ f-chaty-native-new/
├── relay-server/ ✅ done ├── relay-server/ ✅ done
└── FchatiApp/ └── FchatyApp/
├── Package.swift no Xcode — SPM only ├── Package.swift no Xcode — SPM only
├── Sources/ ├── Sources/
│ └── FchatiApp/ │ └── FchatyApp/
│ ├── FchatiApp.swift │ ├── FchatyApp.swift
│ ├── Session/ │ ├── Session/
│ │ └── AppSession.swift │ │ └── AppSession.swift
│ ├── Networking/ │ ├── Networking/
@ -114,7 +114,7 @@ f-chaty-native-new/
│ │ └── PairingViewModel.swift │ │ └── PairingViewModel.swift
│ └── Settings/ │ └── Settings/
│ └── SettingsView.swift │ └── SettingsView.swift
└── FchatiApp.entitlements └── FchatyApp.entitlements
``` ```
--- ---
@ -132,31 +132,31 @@ f-chaty-native-new/
**Requirements:** **Requirements:**
Create `FchatiApp/Package.swift`: Create `FchatyApp/Package.swift`:
```swift ```swift
// swift-tools-version: 5.9 // swift-tools-version: 5.9
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "FchatiApp", name: "FchatyApp",
platforms: [.macOS(.v14)], platforms: [.macOS(.v14)],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "FchatiApp", name: "FchatyApp",
path: "Sources/FchatiApp" path: "Sources/FchatyApp"
) )
] ]
) )
``` ```
Create `FchatiApp/Sources/FchatiApp/FchatiApp.swift`: Create `FchatyApp/Sources/FchatyApp/FchatyApp.swift`:
- `@main` SwiftUI App - `@main` SwiftUI App
- Uses `MenuBarExtra` with `.window` style - Uses `MenuBarExtra` with `.window` style
- Menu bar icon: `Image(systemName: "message")` - Menu bar icon: `Image(systemName: "message")`
- Popover width: 360pt, min height: 480pt - Popover width: 360pt, min height: 480pt
- A root view with three tabs: Chat, Pairing, Settings (segmented picker at top) - A root view with three tabs: Chat, Pairing, Settings (segmented picker at top)
Create `FchatiApp/FchatiApp.entitlements`: Create `FchatyApp/FchatyApp.entitlements`:
```xml ```xml
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@ -172,7 +172,7 @@ Create `FchatiApp/FchatiApp.entitlements`:
</plist> </plist>
``` ```
Verify: `swift build` succeeds from `FchatiApp/` directory. Verify: `swift build` succeeds from `FchatyApp/` directory.
--- ---
@ -201,7 +201,7 @@ Verify: `swift build` succeeds from `FchatiApp/` directory.
- [x] TASK-05 — AppSession (State Machine) (done 2026-07-26) - [x] TASK-05 — AppSession (State Machine) (done 2026-07-26)
**Priority:** Third — after TASK-02, TASK-03, TASK-04 **Priority:** Third — after TASK-02, TASK-03, TASK-04
**File:** `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` **File:** `FchatyApp/Sources/FchatyApp/Session/AppSession.swift`
**Depends on:** `KeychainStore`, `RelayAPI`, `WSClient` **Depends on:** `KeychainStore`, `RelayAPI`, `WSClient`
**Requirements:** **Requirements:**
@ -296,7 +296,7 @@ Both modes observe `AppSession.shared.state` and navigate to `ChatView` when sta
- [x] TASK-08 — MessageStore (Local Persistence) (done 2026-07-26) - [x] TASK-08 — MessageStore (Local Persistence) (done 2026-07-26)
**Priority:** Fourth (parallel) **Priority:** Fourth (parallel)
**File:** `FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift` **File:** `FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift`
**Independent:** Yes **Independent:** Yes
**Requirements:** **Requirements:**
@ -328,7 +328,7 @@ actor MessageStore {
} }
``` ```
Storage: JSON file at `FileManager.default.urls(for: .applicationSupportDirectory, ...).first!.appendingPathComponent("de.diyaa.fchati/messages.json")`. Storage: JSON file at `FileManager.default.urls(for: .applicationSupportDirectory, ...).first!.appendingPathComponent("de.diyaa.fchaty/messages.json")`.
Create the directory if it does not exist. Create the directory if it does not exist.
--- ---
@ -355,7 +355,7 @@ Simple settings screen:
- [x] TASK-10 — New Message Notifications (done 2026-07-26) - [x] TASK-10 — New Message Notifications (done 2026-07-26)
**Priority:** Fifth (independent) **Priority:** Fifth (independent)
**File:** `FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift` **File:** `FchatyApp/Sources/FchatyApp/Notifications/NotificationManager.swift`
**Requirements:** **Requirements:**
@ -381,14 +381,14 @@ Copy the block below, fill in the task number and paste it to your agent:
``` ```
Context: Context:
- Repo: https://git.mohfarawati.de/diyaa/fchaty.git - Repo: https://git.mohfarawati.de/diyaa/fchaty.git
- App folder: FchatiApp/ (Swift Package, no Xcode, SPM only) - App folder: FchatyApp/ (Swift Package, no Xcode, SPM only)
- macOS 14+, Swift 5.9+, no third-party libraries - macOS 14+, Swift 5.9+, no third-party libraries
- Relay server is already running at https://fchaty.diyaa.de - Relay server is already running at https://fchaty.diyaa.de
Task: [paste the full task section here, e.g. TASK-04] Task: [paste the full task section here, e.g. TASK-04]
After writing the code: After writing the code:
1. Make sure it compiles: run `swift build` from the FchatiApp/ directory 1. Make sure it compiles: run `swift build` from the FchatyApp/ directory
2. Fix any compiler errors before finishing 2. Fix any compiler errors before finishing
3. Do not open Xcode or create any .xcodeproj or .xcworkspace files 3. Do not open Xcode or create any .xcodeproj or .xcworkspace files
``` ```

View File

@ -1,26 +1,26 @@
services: services:
fchati-relay: fchaty-relay:
build: . build: .
image: fchati-relay:latest image: fchaty-relay:latest
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
volumes: volumes:
- fchati-files:/data/files - fchaty-files:/data/files
- fchati-queue:/data/queue - fchaty-queue:/data/queue
- fchati-pairing:/data/pairing - fchaty-pairing:/data/pairing
networks: networks:
- traefik-net - traefik-net
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.fchati.rule=Host(`fchaty.diyaa.de`)" - "traefik.http.routers.fchaty.rule=Host(`fchaty.diyaa.de`)"
- "traefik.http.routers.fchati.entrypoints=websecure" - "traefik.http.routers.fchaty.entrypoints=websecure"
- "traefik.http.routers.fchati.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}" - "traefik.http.routers.fchaty.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-myresolver}"
- "traefik.http.services.fchati.loadbalancer.server.port=3000" - "traefik.http.services.fchaty.loadbalancer.server.port=3000"
volumes: volumes:
fchati-files: fchaty-files:
fchati-queue: fchaty-queue:
fchati-pairing: fchaty-pairing:
networks: networks:
traefik-net: traefik-net:

View File

@ -1,5 +1,5 @@
{ {
"name": "fchati-relay", "name": "fchaty-relay",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -485,5 +485,5 @@ wss.on('connection', (ws) => {
// ─── Start ──────────────────────────────────────────────────────────────────── // ─── Start ────────────────────────────────────────────────────────────────────
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Fchati Relay listening on port ${PORT}`); console.log(`Fchaty Relay listening on port ${PORT}`);
}); });

View File

@ -3,13 +3,13 @@
set -euo pipefail set -euo pipefail
REPOSITORY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPOSITORY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROJECT_DIR="$REPOSITORY_DIR/FchatiApp" PROJECT_DIR="$REPOSITORY_DIR/FchatyApp"
BUILD_DIR="$PROJECT_DIR/.build" BUILD_DIR="$PROJECT_DIR/.build"
PRODUCT_NAME="FchatiApp" PRODUCT_NAME="FchatyApp"
INSTALL_PATH="/Applications/Fchati.app" INSTALL_PATH="/Applications/Fchaty.app"
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fchati-install.XXXXXX")" STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fchaty-install.XXXXXX")"
STAGING_APP="$STAGING_DIR/Fchati.app" STAGING_APP="$STAGING_DIR/Fchaty.app"
ICON_PATH="$PROJECT_DIR/Resources/Fchati.icns" ICON_PATH="$PROJECT_DIR/Resources/Fchaty.icns"
cleanup() { cleanup() {
rm -rf "$STAGING_DIR" rm -rf "$STAGING_DIR"
@ -17,7 +17,7 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
if [[ ! -f "$PROJECT_DIR/Package.swift" ]]; then if [[ ! -f "$PROJECT_DIR/Package.swift" ]]; then
echo "FchatiApp package not found: $PROJECT_DIR" >&2 echo "FchatyApp package not found: $PROJECT_DIR" >&2
exit 1 exit 1
fi fi
@ -50,8 +50,8 @@ echo "Creating the application bundle..."
mkdir -p "$STAGING_APP/Contents/MacOS" "$STAGING_APP/Contents/Resources" mkdir -p "$STAGING_APP/Contents/MacOS" "$STAGING_APP/Contents/Resources"
cp "$PROJECT_DIR/Info.plist" "$STAGING_APP/Contents/Info.plist" cp "$PROJECT_DIR/Info.plist" "$STAGING_APP/Contents/Info.plist"
cp "$EXECUTABLE_PATH" "$STAGING_APP/Contents/MacOS/$PRODUCT_NAME" cp "$EXECUTABLE_PATH" "$STAGING_APP/Contents/MacOS/$PRODUCT_NAME"
cp "$ICON_PATH" "$STAGING_APP/Contents/Resources/Fchati.icns" cp "$ICON_PATH" "$STAGING_APP/Contents/Resources/Fchaty.icns"
codesign --force --sign - --timestamp=none --entitlements "$PROJECT_DIR/FchatiApp.entitlements" "$STAGING_APP" codesign --force --sign - --timestamp=none --entitlements "$PROJECT_DIR/FchatyApp.entitlements" "$STAGING_APP"
if [[ -d "$INSTALL_PATH" ]]; then if [[ -d "$INSTALL_PATH" ]]; then
echo "Removing the installed application..." echo "Removing the installed application..."
@ -61,7 +61,7 @@ fi
echo "Installing the application..." echo "Installing the application..."
mv "$STAGING_APP" "$INSTALL_PATH" mv "$STAGING_APP" "$INSTALL_PATH"
echo "Opening Fchati..." echo "Opening Fchaty..."
open "$INSTALL_PATH" open "$INSTALL_PATH"
echo "Fchati was installed and opened successfully." echo "Fchaty was installed and opened successfully."