# MeshMonitor Chat for iOS iOS chat client for [MeshMonitor](https://github.com/Yeraze/meshmonitor) (Meshtastic), Swift port of [meshmonitor-chat.el](https://git.andros.dev/andros/meshmonitor-chat.el). Connects to the MeshMonitor REST API v1 with a Bearer token, browses channels, nodes and direct messages, and lets you read and send messages from your phone. ``` LoRa Radio MeshMonitor iOS +-----------+ +----------------+ +------------------+ | Meshtastic|----->| Web server |----->| MeshMonitor Chat | | Node |<-----| REST API |<-----| (this app) | +-----------+ +----------------+ +------------------+ Physical Proxy + DB SwiftUI device (port 8080) Polling / Send ``` ## Requirements - macOS with Xcode 16+ (tested with Xcode 26.4 / iOS 26 SDK) - iOS 17.0 or later target - [XcodeGen](https://github.com/yonaskolb/XcodeGen) to generate the project: `brew install xcodegen` ## Build The `.xcodeproj` is generated from `project.yml` and is git-ignored. Generate it once: ```sh xcodegen generate open MeshMonitorChat.xcodeproj ``` Then build and run from Xcode (`⌘R`) on a simulator or device. To build from the command line for the simulator: ```sh DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \ xcodebuild -project MeshMonitorChat.xcodeproj \ -scheme MeshMonitorChat \ -destination 'platform=iOS Simulator,name=iPhone 17' \ build ``` To install on a real device, you need to sign the app with your Apple ID. Open the project in Xcode, go to **Signing & Capabilities**, pick your Team and (if needed) change the Bundle Identifier to something unique. ## Screens ### Setup Shown on first launch (and from the gear icon afterwards). Asks for host, port, optional HTTPS toggle and the Bearer token. The "Test connection" button calls `/api/v1/channels`, so it validates both connectivity and the token (a wrong token returns HTTP 401 → `Invalid token (unauthorized)`). When opened from the gear icon, the form also shows a Status section with server version, connection state, local node, uptime and statistics, plus links to the source code and bug tracker. ### Home (tab bar) A bottom `TabView` with four tabs: **Channels**, **Nodes**, **Messages** (DMs) and **Unread**. Each tab is a `NavigationStack` with the Settings gear in the top-left. ### Channels Lists every usable channel (Primary or Secondary) returned by `/api/v1/channels`. Tap a channel to enter its chat. ### Nodes All mesh nodes sorted by hop count, with: - 🟢 / ⚪ online indicator (default threshold: 15 minutes since `lastHeard`). - 🔑 icon if the node has exchanged encryption keys (PKC). - Relative "last heard" time (now / Nm / Nh / Nd). Tap a node to open a DM. Nodes without PKC show an alert ("DM not possible"). ### Direct Messages Active DM conversations, derived from messages with `channel = -1`. The partner is the other side of the message (the local node is filtered out). Sorted by most recent activity. ### Unread Lists nodes with unread DMs. Counted by comparing each message timestamp against a per-node "last read" timestamp persisted in `UserDefaults`. Opening a DM marks it as read up to the latest message. ### Chat Same view for channels and DMs: - Initial fetch: 50 messages. - Polling every 10 seconds with `since=` for incremental updates. - Send with `POST /api/v1/messages` (`channel` for channel chats, `toNodeId` for DMs). - Bubbles: own messages right-aligned (accent color, white text), others left-aligned (`systemGray5`), good contrast in light and dark mode. - Delivery icon on own messages (`circle.dotted` / `checkmark` / `xmark`) reflecting `deliveryState` / `ackFailed`. - Auto-scroll to the bottom when the keyboard opens or new messages arrive. - Return key sends the message; focus stays on the input. - HTTP 413 → "Message too long" alert. HTTP 503 → "Meshtastic node not connected". ## Storage - Host, port and TLS flag: `UserDefaults`. - Bearer token: iOS Keychain (`kSecAttrAccessibleAfterFirstUnlock`). - Per-node last-read timestamps for unread counting: `UserDefaults`. ## Project layout ``` MeshMonitorChat/ ├── App/ # MeshMonitorChatApp + RootView ├── Models/ # Channel, Message, Node, ServerStatus, ChatTarget ├── Services/ # APIClient, Settings, KeychainStore, │ # MeshDataStore, ReadTracker ├── Views/ # SetupView, WelcomeView, ChannelListView, │ # NodeListView, DMListView, UnreadListView, │ # ChatView, MessageRow ├── Resources/ # Assets.xcassets (AppIcon, AccentColor) └── Info.plist project.yml # XcodeGen spec ``` The `.xcodeproj` is generated from `project.yml`, never edited by hand. ## API endpoints used | Method | Path | Used by | |--------|------------------------------------------|----------------------------------| | GET | `/api/v1/status` + `/api/status` (fetched in parallel and merged) | Status section, Test conn | | GET | `/api/v1/channels` | Channel list, Test connection | | GET | `/api/v1/nodes` | Node list, sender resolution | | GET | `/api/v1/messages?channel=N&limit=&since=` | Channel chat | | GET | `/api/v1/messages?toNodeId=&fromNodeId=` | DM chat (both directions merged) | | GET | `/api/v1/messages?limit=200` | DM list, Unread aggregation | | POST | `/api/v1/messages` (`text`, `channel`) | Send to channel | | POST | `/api/v1/messages` (`text`, `toNodeId`) | Send DM | All requests carry `Authorization: Bearer `. HTTP 401/403 are mapped to `APIError.unauthorized` so the UI can show a clear message. ## Notes - Timestamps in messages may arrive as Unix seconds, Unix milliseconds or ISO-8601 strings; the `Message` decoder normalises all three. - `NSAllowsLocalNetworking` is enabled so the app can reach local IPs (`192.168.x.x`) over plain HTTP. Arbitrary public-internet HTTP is blocked; use HTTPS for remote servers. - Implemented: channel and DM read/send, reactions (long-press a message), replies, delivery state, unread counters. Traceroute and position request (which use a session+CSRF API in the upstream) are not implemented yet. ## License GPL-3.0-or-later, matching the upstream Emacs package.