Files
andros d8a6299600 Identify own messages reliably and tidy the Setup screen
Recognise own messages by id (prefix and case insensitive) or by
node number, so chat bubbles align right with the accent colour even
when the message only carries fromNodeNum. Fetch /api/v1/status and
/api/status in parallel and merge them, since MeshMonitor 4 dropped
localNode from the legacy endpoint.

Add a "How to create a token" link, plus Source code and Report a
bug links to the Settings screen. Default the server port to 8080
(MeshMonitor's Docker default). Add PRIVACY.md, refresh the README
and ignore CLAUDE.md.
2026-05-07 10:01:15 +02:00

6.4 KiB

MeshMonitor Chat for iOS

iOS chat client for MeshMonitor (Meshtastic), Swift port of 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 to generate the project: brew install xcodegen

Build

The .xcodeproj is generated from project.yml and is git-ignored. Generate it once:

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:

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=<unix> 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 <token>. 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.