- Swift 99.1%
- Shell 0.5%
- Makefile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| AnotherIM | ||
| AnotherIME2ETests | ||
| AnotherIMNotificationService | ||
| AnotherIMShared | ||
| AnotherIMTests | ||
| AnotherIMUITests | ||
| Packages | ||
| tacky@c95b3d9d18 | ||
| .gitignore | ||
| .gitmodules | ||
| .swiftformat | ||
| .swiftlint.yml | ||
| AnotherTacky.Package.swift | ||
| COPYING | ||
| coverage.py | ||
| LICENSE | ||
| Makefile | ||
| PRIVACY.md | ||
| project.yml | ||
| README.md | ||
| remote.sh | ||
| TACKY-MIGRATION.md | ||
Another.im XMPP client for iOS and macOS
This is the source code for the another.im XMPP app: one SwiftUI app for the iPhone and a native one for the Mac, over the same backend.
Layout
| path | what it is |
|---|---|
AnotherIM/iOS |
the iOS app: entry point, the delegate for push, app icon and launch screen |
AnotherIM/Mac |
the macOS app: entry point, the Settings window, calls without CallKit |
AnotherIM/App |
AppEnvironment (the backend, the PIN gate, push, the scene phase) and the app's settings |
AnotherIM/Shell |
the UI's root: the gate (lock, welcome) and AppShell, which joins the features |
AnotherIM/Live |
the features' clients over the backend (*Client+Live.swift) |
AnotherIM/Calls |
calls: CallsModel, WebRTC, CallKit |
AnotherIM/Tacky, AnotherIM/XMPP |
the backend's protocol (TackyAPI, the generated TackyWire) and the app's XMPP layer |
AnotherIMShared |
what the app and the notification service extension share: keys, paths, push |
AnotherIMNotificationService |
the notification service extension |
Packages/ |
the UI, as local Swift packages (below) |
The UI is in Packages/. AnotherDesign holds the colours, type roles,
metrics and the glass; AnotherComponents the views more than one screen
shows (and AnotherFormat, their text); AnotherModels the values the
screens show and the features' clients, closures with fixtures beside them.
Each Feature* package (Lock, Onboarding, Settings, ChatList, Compose,
Conversation, ChatDetails, Media, Security, Calls) has a …Core target,
the screens' logic in Foundation and Observation, tested with Swift Testing
on Linux too, and a …UI target with the SwiftUI views. Features never
import each other: what one asks of another is a callback AppShell
answers. The live clients are in AnotherIM/Live, the fixtures in
AnotherModelsFixtures (previews and tests).
Building from a clean clone (on a Mac)
The app is the fifth host of Tacky's JSON protocol (TACKY-MIGRATION.md §2,
§8): it links the backend's iOS archive, AnotherTacky.xcframework, as a
Swift package (AnotherTacky, C module CTacky). The backend is the nested
submodule tacky/ (libtacky, pinned by this repository; its
own submodules are third-party pins), so a clone of this repository alone
has everything the build reads, and the build writes only under build/.
What the Mac needs:
- Xcode 26 or later (the iOS 26 and macOS 26 SDKs; the app runs from iOS 18.1
and macOS 15), with the iOS 18, 26 and 27 simulator runtimes
(
xcode-select -ppointing at it; the project is Swift 6, the backend'sPackage.swiftswift-tools 6.0); - Homebrew:
brew install make cmake ninja xcodegen swiftlint-- GNU make 4.3 or later isgmake(macOS's ownmakeis 3.81, too old for the backend'splatforms/ios.mk). No Tcl is needed: thetclsh9.0that bundles the backend's script image is one on PATH or, failing that, built bygmake backendfrom the backend's pinned Tcl 9.0 underbuild/backend/host(Homebrew'stcl-tkis 9.1); - Rust through rustup, with the two iOS targets (the macOS slice is the
host's own, aarch64-apple-darwin):
rustup target add aarch64-apple-ios aarch64-apple-ios-sim; - the network on the first build: the backend's make fetches the Tcl, tcllib and LibreSSL source tarballs (pinned by checksum), and xcodebuild resolves the app's other Swift packages.
git clone --recurse-submodules ssh://git@dev.another.im:3022/another-im/anotherim-swift.git
cd anotherim-swift
gmake deps # only if the clone was not recursive: the nested backend
gmake backend # build/backend/AnotherTacky: the xcframework + Package.swift
gmake app-ios # xcodegen, then an unsigned simulator build
gmake test # AnotherIMTests on a simulator
gmake test-packages # swift test in every package (on Linux too)
gmake archive # a Release device archive, signed as project.yml says
gmake help lists every target. What they are:
| target | what it does |
|---|---|
backend (xcframework) |
make -C tacky -f platforms/ios.mk libtacky-ios with every tree under build/backend: per slice LibreSSL and the Rust archives -- tacky-core, tacky-crypto, tacky-omemo (ios/), the interpreter, its Tcl sources and the merged archive (tacky/ios-<sdk>), the LibreSSL tarball (dl/), and the package project.yml links (AnotherTacky/); then the backend's native lib for macOS (macos/), folded into the xcframework as a third slice, with AnotherTacky.Package.swift as the package |
project |
xcodegen generate from project.yml |
app-ios |
simulator build (IOS_DESTINATION, BUILD_SIGNING=CODE_SIGNING_ALLOWED=NO, CONFIG=Debug) |
app-macos |
the native macOS app on an Apple-silicon Mac (MACOS_DESTINATION), against the backend's macOS slice |
app |
both of the above |
archive |
build/AnotherIM.xcarchive (ARCHIVE_DESTINATION=generic/platform=iOS, ARCHIVE_SIGNING, e.g. DEVELOPMENT_TEAM=…) |
test |
xcodebuild test on TEST_DESTINATION (platform=iOS Simulator,name=iPhone 16), results in build/AnotherIM.xcresult |
test-packages |
swift test in every package under Packages/ (on Linux, their …Core targets alone) |
clean, clean-backend, distclean |
the Xcode project and DerivedData; build/backend; both |
XCODEBUILD_FLAGS goes on every xcodebuild line (-allowProvisioningUpdates,
-quiet). app-*, archive and test build the backend first only when
build/backend/AnotherTacky/Package.swift is missing; gmake backend
rebuilds it on request.
The nested checkout is only read, with one exception the backend's make does
not yet let a caller move: its ios-lib copies each slice's archive to
tacky/dist/libtacky-<sdk>.a (ignored by the backend's .gitignore) before
platforms/ios.mk folds the two.
From the another-im workspace
TACKY= names a backend checkout other than the nested one; the output
still lands in build/backend, so project.yml is the same:
gmake -C anotherim-swift app-ios TACKY=../another-tacky
or, with libtacky cloned beside this repository,
gmake app-ios TACKY=../libtacky.
From a Linux machine the workspace's mac/run.sh (MAC_HOST=user@mac)
syncs the tree to a Mac and runs a command there, e.g.
mac/run.sh gmake -C anotherim-swift backend app-ios TACKY=../another-tacky.
From a machine without Xcode (remote.sh)
remote.sh lets an editor without Xcode (a Linux container that mounts
this checkout) build, test and drive the app on a Mac. On the Mac, in the
checkout: ./remote.sh serve (the watchman). Elsewhere:
./remote.sh [@18|@26|@27] <verb>, e.g. build ios|mac, test,
pkgtest <Package>, analyze ios|mac (swiftlint's unused code and
imports), run [mac], shot [mac], tap-id <id>, logs [mac]; @NN
picks the simulator's iOS. ./remote.sh --help lists every verb. The two
sides talk through .remote/ (ignored by git); a build's full log is
.remote/out/<job>.log.
What has run, and what has not
On 2026-10-10 the steps above ran on an Apple-silicon Mac with Xcode 27.0
(iOS 27.0 SDK and simulator), from a clean build/ and with no Homebrew
Tcl: gmake backend (with its host tclsh9.0), gmake app-ios,
gmake app-macos (unsigned), and gmake test on
TEST_DESTINATION='platform=iOS Simulator,name=iPhone 17' (Xcode 27 has no
iPhone 16 simulator) -- 339 tests, no failure. The app was installed on that
simulator and opens on its PIN screen. Not run yet: gmake archive and a
signed app-macos (both need the team's signing identities), the app on a
device, and the app past its PIN screen. The user's rule behind the layout
(2026-09-24): a module's repository is cloned and built with make from
it, without the superproject's contents.
The store and the PIN
The backend's store is in two tiers (libtacky 1.0.4, storage split):
accounts.db, which a call answered on the lock screen opens with the
device key alone (AppDeviceKey), and every account's history, roster and
keys, which open only with the PIN together with Face ID, Touch ID or the
passcode, or with Face ID or Touch ID alone, through a second key the
Secure Enclave releases only inside that authentication
(AppPresenceKey). A store from before is split on the first unlock after
the update. TACKY-MIGRATION.md §8.2 has the requests. The enclave's path is
compiled and was not run on a device: the tests take the software key.
Push
The app holds two APNs tokens and registers each with the server's App
Server, apushd (push.another.im, XEP-0357 through libtacky's
push set_target), under its own apushd module name, AppPushConfiguration:
| token | pushModule (sandbox / production) |
apushd module | wakes |
|---|---|---|---|
APNs (registerForRemoteNotifications) |
anotherimIOSdev / anotherimIOSprod |
pushType: alert, topic im.narayana.anotherim.ios |
the notification service extension, for a message |
PushKit (PKPushRegistry, .voIP) |
anotherimIOSVoipdev / anotherimIOSVoipprod |
pushType: voip, topic im.narayana.anotherim.ios.voip |
the app, for a call (CallKit) |
Two names because a module has one topic and one delivery class, so a
PushKit token can only be delivered through a voip module. The PushKit
registration carries apushCalls: only, which tells ejabberd's mod_apush
(anotherim-server) to push it calls and nothing else; the APNs one carries
none, which is messages only. Both carry the app's own X25519 key
(apushEncryptionPublicKey, TACKY-MIGRATION.md D7): mod_apush seals a
JSON summary to it (who, how many, a call's session and media; never a
message's text) under the label apush-v2, apushd passes it on unread as
apushEncrypted, and the extension or the app opens it here
(AppPushEncryption).
A message push carries the chat's opaque collapse tag (apushCollapse,
which apushd sends as apns-collapse-id), so a chat's newer notice
replaces the older one, and mod_apush holds a burst in one chat to one
push and one more after its window with the latest count. iOS names a
pushed notice itself (a random id or that tag), so the extension files it
under the chat's thread identifier (AppPushThread, <account>|<chat>)
and the app withdraws a chat's notices by that thread.
The four modules are in anotherim-server's push/settings.sh, which writes
apushd's settings.json: anotherimIOSdev and anotherimIOSprod with
"pushType": "alert" and the bundle id as topic, anotherimIOSVoipdev
and anotherimIOSVoipprod with "pushType": "voip" and the bundle id plus
.voip; the dev pair against the APNs sandbox, the prod pair against
production. The app picks the pair by the aps-environment of the profile
it is signed with (AppPushConfiguration.isSandbox), not by Debug or
Release: a token sent to the other environment is 400 BadDeviceToken.
License
Copyright (c) 2024 Narayana OÜ
Licensed under the Mozilla Public License 2.0 (LICENSE). The backend it
embeds, libtacky (the nested tacky/), is GPL-3.0, so an app build that links
it is distributed under the GPL-3.0 terms as a whole; MPL-2.0 is compatible
with that.