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/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@ -5,15 +5,15 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>FchatiApp</string>
<string>FchatyApp</string>
<key>CFBundleIdentifier</key>
<string>de.diyaa.fchati</string>
<string>de.diyaa.fchaty</string>
<key>CFBundleIconFile</key>
<string>Fchati.icns</string>
<string>Fchaty.icns</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Fchati</string>
<string>Fchaty</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@ -25,6 +25,6 @@
<key>NSHighResolutionCapable</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>Fchati uses the microphone to record voice messages.</string>
<string>Fchaty uses the microphone to record voice messages.</string>
</dict>
</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
@main
struct FchatiApp: App {
struct FchatyApp: App {
var body: some Scene {
WindowGroup("Fchati") {
WindowGroup("Fchaty") {
RootView()
}
.defaultSize(width: 420, height: 560)
@ -79,8 +79,8 @@ private struct RootView: View {
} label: {
Image(systemName: "power")
}
.accessibilityLabel("Quit Fchati")
.help("Quit Fchati")
.accessibilityLabel("Quit Fchaty")
.help("Quit Fchaty")
}
.padding()

View File

@ -16,6 +16,19 @@ struct ChatView: 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) {
ScrollViewReader { proxy in
ScrollView {
@ -61,9 +74,7 @@ struct ChatView: View {
TextField("Message", text: $viewModel.draft)
.textFieldStyle(.roundedBorder)
.onSubmit {
Task {
await viewModel.sendText()
}
Task { await viewModel.sendText() }
}
Image(systemName: viewModel.isRecording ? "mic.fill" : "mic")
@ -72,19 +83,13 @@ struct ChatView: View {
.contentShape(Circle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
viewModel.startRecording()
}
.onEnded { _ in
viewModel.stopRecordingAndSend()
}
.onChanged { _ in viewModel.startRecording() }
.onEnded { _ in viewModel.stopRecordingAndSend() }
)
.help("Hold to record audio")
Button("Send") {
Task {
await viewModel.sendText()
}
Task { await viewModel.sendText() }
}
.buttonStyle(.borderedProminent)
.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() {
let panel = NSOpenPanel()
panel.canChooseDirectories = false

View File

@ -58,6 +58,19 @@ final class ChatViewModel: ObservableObject {
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 {
let url = try makeRecordingURL()
let settings: [String: Any] = [
@ -83,6 +96,7 @@ final class ChatViewModel: ObservableObject {
}
}
func stopRecordingAndSend() {
guard isRecording, let recorder, let recordingURL else {
return
@ -106,7 +120,7 @@ final class ChatViewModel: ObservableObject {
create: true
)
let recordingsDirectory = cachesDirectory.appendingPathComponent(
"FchatiRecordings",
"FchatyRecordings",
isDirectory: true
)

View File

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

View File

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

View File

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

View File

@ -209,7 +209,7 @@ final class AppSession: ObservableObject {
KeychainStore.peerID = peerID
KeychainStore.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":

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,18 @@
### What was built
- `FchatiApp/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
- `FchatiApp/FchatiApp.entitlements` — sandbox + outgoing/incoming network
- `FchatiApp/Info.plist` — bundle ID `de.diyaa.fchati`, version 0.1.0, `LSUIElement = YES` (hides Dock icon)
- `FchatiApp/Tests/FchatiAppTests/AppTabTests.swift` — verifies all three tabs and their icons exist
- `FchatyApp/Package.swift` — SPM package, macOS 14+, no Xcode
- `FchatyApp/Sources/FchatyApp/FchatyApp.swift` — MenuBarExtra (.window style), 360pt wide, 480pt min height, three tabs (Chat / Pairing / Settings) with segmented picker
- `FchatyApp/FchatyApp.entitlements` — sandbox + outgoing/incoming network
- `FchatyApp/Info.plist` — bundle ID `de.diyaa.fchaty`, version 0.1.0, `LSUIElement = YES` (hides Dock icon)
- `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
### Verified
- `swift build` passes (debug)
- `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
@ -33,14 +33,14 @@
### 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.
- Token and peer properties support read, update, and deletion through Swift property syntax.
- `clearAll()` removes pairing credentials while preserving the installation ID.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
## TASK-03 — RelayAPI (HTTP) ✅
@ -48,15 +48,15 @@
### 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.
- 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`.
- `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
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with the relay response tests.
## TASK-04 — WSClient (WebSocket) ✅
@ -65,15 +65,15 @@
### 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`.
- 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.
- `FchatiApp/Tests/FchatiAppTests/WSMessageTests.swift` — serialization coverage for relay WebSocket messages.
- `FchatyApp/Tests/FchatyAppTests/WSMessageTests.swift` — serialization coverage for relay WebSocket messages.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with the WebSocket message test.
## TASK-05 — AppSession (State Machine) ✅
@ -82,7 +82,7 @@
### 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.
- 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.
@ -93,9 +93,9 @@
### 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.
- `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 ✅
@ -103,12 +103,12 @@
### 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.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with session storage coverage.
## TASK-06 — PairingView ✅
@ -117,15 +117,15 @@
### 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.
- `FchatiApp/Sources/FchatiApp/Features/Pairing/PairingViewModel.swift` — pairing request orchestration, code normalization, and display-state management.
- `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.
- `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.
- 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
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with pairing view-model coverage.
## TASK-07 — ChatView ✅
@ -134,15 +134,15 @@
### 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.
- `FchatiApp/Sources/FchatiApp/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.
- `FchatiApp/FchatiApp.entitlements` and `FchatiApp/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/Sources/FchatyApp/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/ChatViewModel.swift` — message and file sending, `.m4a` recording lifecycle, temporary recording cleanup, and inline send errors.
- `FchatyApp/Sources/FchatyApp/Features/Chat/MessageBubble.swift` — left and right message bubbles, Markdown body rendering, attachment metadata, and timestamps.
- `FchatyApp/FchatyApp.entitlements` and `FchatyApp/Info.plist` — microphone sandbox entitlement and privacy usage description required for voice recording.
- `FchatyApp/Tests/FchatyAppTests/ChatMessageBubbleTests.swift` — Markdown rendering coverage for message bodies.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with chat view coverage.
## TASK-09 — SettingsView ✅
@ -151,12 +151,12 @@
### 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.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with all current application tests.
## Integration Update ✅
@ -165,13 +165,13 @@
### 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.
- A successful pairing switches the selected tab to chat.
### Verified
- `swift build` passes from `FchatiApp/`.
- `swift build` passes from `FchatyApp/`.
- `swift test` passes with all current application tests.
## 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.
- 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.
- `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.
## Bug Fixes — Post-Review ✅
@ -208,7 +208,7 @@
### 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.
@ -219,8 +219,8 @@
### Fix 2 — Message timestamps for queued/offline messages
**Files:**
- `FchatiApp/Sources/FchatiApp/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/Networking/WSClient.swift` — added `sentAt: String?` field to `WSMessage`
- `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.
@ -236,7 +236,7 @@
**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`
- `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`.
@ -247,8 +247,8 @@
### 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
- `FchatyApp/Sources/FchatyApp/Session/AppSession.swift` — added `markVisibleMessagesRead()`: marks all incoming unread messages as read and sends `{ type: "read", messageIDs: [...] }` via WebSocket
- `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.

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.
> 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.
**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.**
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
The Mac app (`FchatiApp/`) does not exist yet.
The Mac app (`FchatyApp/`) does not exist yet.
### Pending server tasks
@ -90,11 +90,11 @@ The Mac app (`FchatiApp/`) does not exist yet.
```
f-chaty-native-new/
├── relay-server/ ✅ done
└── FchatiApp/
└── FchatyApp/
├── Package.swift no Xcode — SPM only
├── Sources/
│ └── FchatiApp/
│ ├── FchatiApp.swift
│ └── FchatyApp/
│ ├── FchatyApp.swift
│ ├── Session/
│ │ └── AppSession.swift
│ ├── Networking/
@ -114,7 +114,7 @@ f-chaty-native-new/
│ │ └── PairingViewModel.swift
│ └── Settings/
│ └── SettingsView.swift
└── FchatiApp.entitlements
└── FchatyApp.entitlements
```
---
@ -132,31 +132,31 @@ f-chaty-native-new/
**Requirements:**
Create `FchatiApp/Package.swift`:
Create `FchatyApp/Package.swift`:
```swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "FchatiApp",
name: "FchatyApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "FchatiApp",
path: "Sources/FchatiApp"
name: "FchatyApp",
path: "Sources/FchatyApp"
)
]
)
```
Create `FchatiApp/Sources/FchatiApp/FchatiApp.swift`:
Create `FchatyApp/Sources/FchatyApp/FchatyApp.swift`:
- `@main` SwiftUI App
- Uses `MenuBarExtra` with `.window` style
- Menu bar icon: `Image(systemName: "message")`
- Popover width: 360pt, min height: 480pt
- A root view with three tabs: Chat, Pairing, Settings (segmented picker at top)
Create `FchatiApp/FchatiApp.entitlements`:
Create `FchatyApp/FchatyApp.entitlements`:
```xml
<?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">
@ -172,7 +172,7 @@ Create `FchatiApp/FchatiApp.entitlements`:
</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)
**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`
**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)
**Priority:** Fourth (parallel)
**File:** `FchatiApp/Sources/FchatiApp/Storage/MessageStore.swift`
**File:** `FchatyApp/Sources/FchatyApp/Storage/MessageStore.swift`
**Independent:** Yes
**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.
---
@ -355,7 +355,7 @@ Simple settings screen:
- [x] TASK-10 — New Message Notifications (done 2026-07-26)
**Priority:** Fifth (independent)
**File:** `FchatiApp/Sources/FchatiApp/Notifications/NotificationManager.swift`
**File:** `FchatyApp/Sources/FchatyApp/Notifications/NotificationManager.swift`
**Requirements:**
@ -381,14 +381,14 @@ Copy the block below, fill in the task number and paste it to your agent:
```
Context:
- 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
- Relay server is already running at https://fchaty.diyaa.de
Task: [paste the full task section here, e.g. TASK-04]
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
3. Do not open Xcode or create any .xcodeproj or .xcworkspace files
```

View File

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

View File

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

View File

@ -485,5 +485,5 @@ wss.on('connection', (ws) => {
// ─── Start ────────────────────────────────────────────────────────────────────
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
REPOSITORY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROJECT_DIR="$REPOSITORY_DIR/FchatiApp"
PROJECT_DIR="$REPOSITORY_DIR/FchatyApp"
BUILD_DIR="$PROJECT_DIR/.build"
PRODUCT_NAME="FchatiApp"
INSTALL_PATH="/Applications/Fchati.app"
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fchati-install.XXXXXX")"
STAGING_APP="$STAGING_DIR/Fchati.app"
ICON_PATH="$PROJECT_DIR/Resources/Fchati.icns"
PRODUCT_NAME="FchatyApp"
INSTALL_PATH="/Applications/Fchaty.app"
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fchaty-install.XXXXXX")"
STAGING_APP="$STAGING_DIR/Fchaty.app"
ICON_PATH="$PROJECT_DIR/Resources/Fchaty.icns"
cleanup() {
rm -rf "$STAGING_DIR"
@ -17,7 +17,7 @@ cleanup() {
trap cleanup EXIT
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
fi
@ -50,8 +50,8 @@ echo "Creating the application bundle..."
mkdir -p "$STAGING_APP/Contents/MacOS" "$STAGING_APP/Contents/Resources"
cp "$PROJECT_DIR/Info.plist" "$STAGING_APP/Contents/Info.plist"
cp "$EXECUTABLE_PATH" "$STAGING_APP/Contents/MacOS/$PRODUCT_NAME"
cp "$ICON_PATH" "$STAGING_APP/Contents/Resources/Fchati.icns"
codesign --force --sign - --timestamp=none --entitlements "$PROJECT_DIR/FchatiApp.entitlements" "$STAGING_APP"
cp "$ICON_PATH" "$STAGING_APP/Contents/Resources/Fchaty.icns"
codesign --force --sign - --timestamp=none --entitlements "$PROJECT_DIR/FchatyApp.entitlements" "$STAGING_APP"
if [[ -d "$INSTALL_PATH" ]]; then
echo "Removing the installed application..."
@ -61,7 +61,7 @@ fi
echo "Installing the application..."
mv "$STAGING_APP" "$INSTALL_PATH"
echo "Opening Fchati..."
echo "Opening Fchaty..."
open "$INSTALL_PATH"
echo "Fchati was installed and opened successfully."
echo "Fchaty was installed and opened successfully."