XMPP client written in JavaScript that works both in browser and as native app using Tauri.rs
  • TypeScript 79.7%
  • Rust 6.3%
  • Kotlin 4.6%
  • JavaScript 3%
  • Python 2.8%
  • Other 3.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Paavel A. 99640a8451
The 0.4.7 notes name the Day chat ground, the lighter field and Use threads
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 14:29:27 +03:00
docker Merge release/0.4.7 into fix/review-views: the settings dialog, the login screen rework, libtacky v1.0.0-beta7 and the shell, Android and UI-state review fixes, on top of the views and tooling fixes. Resolved: test-shell keeps its loop over SHELL_TEST_MODULES, which now also lists config_file beside the release's device_key (22 suites); worker/Makefile drops both the release's spike and this branch's fake peer, so test-worker is five suites (boot, omemo, host, store, page) and the README's row says so; the theme test's Ocean list loses --rail with the token (25). tools/screenshots-webkit.py stays deleted. Follow-ups in the merge: the sign-in card's QR screen (signin-qr.ts) reads codes through views/qr-scanner.ts like verify keys and the relink sheet; the tune icon comes back for the sign-in card; the storage gate's repeat field reuses "Repeat the passphrase" and "The two do not match yet." (the typed-passphrase path only; the recovery-phrase flow of the encrypt-storage sheet is untouched); state/app-lock.ts names no Eesti app. en.json is regenerated, "1 min ago" and "{n} mins ago" leave every catalogue, and the new strings ("Sign in as {jid}?", its explanation, "one address per line", the plural "{n} min ago") are translated in et, fi, lt, lv, ru and uk. In the container: make check (i18n-check STRICT clean) test-ui (148 files, 2005 tests) test-shell (22 suites) test-worker (5 suites, 160 checks) pass. 2026-10-12 11:14:00 +03:00
docs Every AndroidShell method the page calls is a plain method of the interface: the page and MainActivity's ShellBridge ship in the same APK (both flavours implement all of them), so an older shell never runs the current page and the optional markers, the "older shell" wording and the guards behind them were dead. Gone: the typeof and ?. checks in delivery (backgroundStatus, requestBatteryExemption, setConnectedService), links and sso (openLink), oauth-signin (takeOAuthRedirect), os-links (takeDeepLink), shell-mutes (pushMutes), log-view (openLog, and its "can only be copied" note), storage and encryption-offer (deviceUnlock, deviceUnlockSaved), android-files (the six file methods), preferences (buildKind), theme (setSystemBars, now through androidShell()) and callShell's method check, with AndroidCallShell a plain Pick; the delivery sheet's "This version of the app cannot stay connected" card goes too (an unparsable background status now reads as nothing on), its strings leave every catalogue and docs/delivery.md loses the row. encryption-offer keeps its optional deviceKeyHardware probe, which no shell has yet. The tests build shells from one full fake (test/android-shell-fake.ts) instead of partial objects, and the tests of an older shell are gone. In the container: make check test-ui (148 files, 2003 tests) pass. 2026-10-12 11:21:29 +03:00
flatpak The 0.4.7 notes name the Day chat ground, the lighter field and Use threads 2026-10-12 14:29:27 +03:00
libtacky@8c3fa4d54d The app is on libtacky v1.0.0-beta7 (8c3fa4d5), which 0.4.7 ships on, and ui/src/protocol/generated/api.ts is regenerated from its schema (libtacky's tools/api/api.py gen --ts): account oauth_begin's client_id and authorization_endpoint and its answer's flow and redirect_uri, oauth_finish's url (code and state now optional), register connect's challenge and register set_challenge, storage encrypt's key, omemo <LinkVerified> and <LinkVerifyFailed>, and beta7's other additions. Nothing calls the new words yet; the next commit drops the beta6 paths that stood in for them. 2026-10-12 10:29:48 +03:00
src-tauri background_set takes the app handle again: the tray's Stay connected needs it, which the shell review's removal of the then-unused argument crossed 2026-10-12 12:00:00 +03:00
tools Day's message field is lighter, #f4f7fb on the white bar (the user, 2026-10-12: "i'd like to see the input bar lighter") 2026-10-12 14:24:23 +03:00
ui Day's message field is lighter, #f4f7fb on the white bar (the user, 2026-10-12: "i'd like to see the input bar lighter") 2026-10-12 14:24:23 +03:00
worker Merge release/0.4.7 into fix/review-views: the settings dialog, the login screen rework, libtacky v1.0.0-beta7 and the shell, Android and UI-state review fixes, on top of the views and tooling fixes. Resolved: test-shell keeps its loop over SHELL_TEST_MODULES, which now also lists config_file beside the release's device_key (22 suites); worker/Makefile drops both the release's spike and this branch's fake peer, so test-worker is five suites (boot, omemo, host, store, page) and the README's row says so; the theme test's Ocean list loses --rail with the token (25). tools/screenshots-webkit.py stays deleted. Follow-ups in the merge: the sign-in card's QR screen (signin-qr.ts) reads codes through views/qr-scanner.ts like verify keys and the relink sheet; the tune icon comes back for the sign-in card; the storage gate's repeat field reuses "Repeat the passphrase" and "The two do not match yet." (the typed-passphrase path only; the recovery-phrase flow of the encrypt-storage sheet is untouched); state/app-lock.ts names no Eesti app. en.json is regenerated, "1 min ago" and "{n} mins ago" leave every catalogue, and the new strings ("Sign in as {jid}?", its explanation, "one address per line", the plural "{n} min ago") are translated in et, fi, lt, lv, ru and uk. In the container: make check (i18n-check STRICT clean) test-ui (148 files, 2005 tests) test-shell (22 suites) test-worker (5 suites, 160 checks) pass. 2026-10-12 11:14:00 +03:00
.gitignore More dead code goes: exports used only in their own file are local (linkify's isSafeHref and withoutCode, styled's styledBlock and styledLine, message-row's tick, presence-sheet's SHOWS, quiet-actions' muteChoiceLabel and confirmBlock, window-drag's DRAG_ATTR); AimSheet.typed, read only by a test that checks the behaviour anyway; <aim-profile-setup>'s acc, which it never read; two doc comments that had drifted off their functions (the nick line's, shotBox's) sit on them again. docker/appimage.sh drops the edition's uidist, overlay and suffix (one product since 0.14) and the second copy of the target-directory paragraph, and tools/screenshots-webkit.py goes, since it ran in a tauri image docker/run.sh no longer has and nothing calls it, with its .gitignore line. 2026-10-12 11:00:30 +03:00
.gitmodules The build follows libtacky v1.0.0-alpha7's layout, built and tested against the release candidate d0d79d4c (a standalone clone, ANOTHER_IM_TACKY), with the submodule pin left at v1.0.0-alpha4 until the tag exists: native/ is gone, so tacky-checkout no longer fetches libomemo-c (OMEMO is the Rust tacky-omemo and the backend needs none of its submodules here), BACKEND_VARS and the Flatpak manifest's make line name TACKY_MEDIA_BUILD and TACKY_MEDIA_DL instead of the OMEMO, libomemo-c and media trees, DEV_WATCH watches tacky/, ext/, tacky-core and tacky-omemo, and tools/flatpak-sources.py vendors the crates of the three Cargo.lock files make lib builds from, there being no workspace lockfile any more. The webrtc media backend is tacky-media/webrtc's libtackymedia_webrtc, a C library with no Tcl in it: webrtc.mk builds it from there without a Tcl prefix and make webrtc no longer waits for libtacky, the Android and Windows goals build it through tacky-media's own webrtc-android and win-webrtc (platforms/webrtc.mk is gone) and macOS through libtacky's webrtc-macos, and the file is libtackymedia_webrtc.{so,dll,dylib} everywhere the shell and the packaging name it (app.rs's WEBRTC_LIB, the AppImage, the Flatpak, the apk's jniLib and MainActivity's loadLibrary, the Windows installer and its smoke run). The browser worker links the new wasm archives (libtacky_omemo.a and libtacky_crypto.a in place of libomemo-c.a and libcrypto.a), calls Tackycore_Init, Tackyomemo_Init and Tackycrypto_Init beside Sqlite3_Init as embed/extensions.h lists them (Tackykeyring, Tackyxml and Tclcrypto are gone), dispatches to ::tacky::core::wire::dispatch, stops with ::tacky::backend destroy and releases acks with ::tacky::xmpp::session::durable_all; its boot and omemo suites use the renamed packages (tacky::core::storage's tacky_store, the typed tacky::core::keyring 0.2, tacky::omemo's ratchet commands, tacky::crypto, tacky::http) and bench-store's seed the renamed wire and store, though its OMEMO rows still use the removed high-level ratchet API. The connection suite is not ported: its fake peer was tacky-xmpp's FakePeer, which libtacky deleted with the crate, and the Tcl peer that replaced it needs tdom, which this image lacks. Checked in the image: make bundle-appimage (dist/another-im-tauri-0.3.6-x86_64.AppImage, 138,668,536 bytes, carrying libtackymedia_webrtc.so), make check clean, test-ui 130 files 1,644 passed, test-shell 17 binaries 93 passed, test-embed 74 and 6 passed against the built archive, the worker's spike 8, boot 35, omemo 3, host 18, store 64 and page 40 checks passed with test-connect failing on the missing crate, and make (dist/web) built. 2026-10-07 22:31:18 +03:00
CHANGELOG.md The 0.4.7 notes name the Day chat ground, the lighter field and Use threads 2026-10-12 14:29:27 +03:00
LICENSE anotherim-web is MPL-2.0 (the user, 2026-09-29: "License is GPL-3.0 for libtacky and MPL 2.0 for anotherim-web and anotherim-swift"): LICENSE is the Mozilla Public License 2.0 text instead of the GPL-3.0's, ui/package.json and its lockfile's root entry and src-tauri/Cargo.toml say MPL-2.0, and the README gains a licence section that says so and that a build linking the GPL-3.0 libtacky (the worker, the Tauri shells) is conveyed under the GPL-3.0 as a whole, which MPL-2.0 permits 2026-09-29 22:56:13 +03:00
Makefile Merge release/0.4.7 into fix/review-views: the settings dialog, the login screen rework, libtacky v1.0.0-beta7 and the shell, Android and UI-state review fixes, on top of the views and tooling fixes. Resolved: test-shell keeps its loop over SHELL_TEST_MODULES, which now also lists config_file beside the release's device_key (22 suites); worker/Makefile drops both the release's spike and this branch's fake peer, so test-worker is five suites (boot, omemo, host, store, page) and the README's row says so; the theme test's Ocean list loses --rail with the token (25). tools/screenshots-webkit.py stays deleted. Follow-ups in the merge: the sign-in card's QR screen (signin-qr.ts) reads codes through views/qr-scanner.ts like verify keys and the relink sheet; the tune icon comes back for the sign-in card; the storage gate's repeat field reuses "Repeat the passphrase" and "The two do not match yet." (the typed-passphrase path only; the recovery-phrase flow of the encrypt-storage sheet is untouched); state/app-lock.ts names no Eesti app. en.json is regenerated, "1 min ago" and "{n} mins ago" leave every catalogue, and the new strings ("Sign in as {jid}?", its explanation, "one address per line", the plural "{n} min ago") are translated in et, fi, lt, lv, ru and uk. In the container: make check (i18n-check STRICT clean) test-ui (148 files, 2005 tests) test-shell (22 suites) test-worker (5 suites, 160 checks) pass. 2026-10-12 11:14:00 +03:00
README.md 0.4.7, on libtacky 1.0.0-beta7 2026-10-12 11:23:22 +03:00
webrtc.mk The build follows libtacky v1.0.0-alpha7's layout, built and tested against the release candidate d0d79d4c (a standalone clone, ANOTHER_IM_TACKY), with the submodule pin left at v1.0.0-alpha4 until the tag exists: native/ is gone, so tacky-checkout no longer fetches libomemo-c (OMEMO is the Rust tacky-omemo and the backend needs none of its submodules here), BACKEND_VARS and the Flatpak manifest's make line name TACKY_MEDIA_BUILD and TACKY_MEDIA_DL instead of the OMEMO, libomemo-c and media trees, DEV_WATCH watches tacky/, ext/, tacky-core and tacky-omemo, and tools/flatpak-sources.py vendors the crates of the three Cargo.lock files make lib builds from, there being no workspace lockfile any more. The webrtc media backend is tacky-media/webrtc's libtackymedia_webrtc, a C library with no Tcl in it: webrtc.mk builds it from there without a Tcl prefix and make webrtc no longer waits for libtacky, the Android and Windows goals build it through tacky-media's own webrtc-android and win-webrtc (platforms/webrtc.mk is gone) and macOS through libtacky's webrtc-macos, and the file is libtackymedia_webrtc.{so,dll,dylib} everywhere the shell and the packaging name it (app.rs's WEBRTC_LIB, the AppImage, the Flatpak, the apk's jniLib and MainActivity's loadLibrary, the Windows installer and its smoke run). The browser worker links the new wasm archives (libtacky_omemo.a and libtacky_crypto.a in place of libomemo-c.a and libcrypto.a), calls Tackycore_Init, Tackyomemo_Init and Tackycrypto_Init beside Sqlite3_Init as embed/extensions.h lists them (Tackykeyring, Tackyxml and Tclcrypto are gone), dispatches to ::tacky::core::wire::dispatch, stops with ::tacky::backend destroy and releases acks with ::tacky::xmpp::session::durable_all; its boot and omemo suites use the renamed packages (tacky::core::storage's tacky_store, the typed tacky::core::keyring 0.2, tacky::omemo's ratchet commands, tacky::crypto, tacky::http) and bench-store's seed the renamed wire and store, though its OMEMO rows still use the removed high-level ratchet API. The connection suite is not ported: its fake peer was tacky-xmpp's FakePeer, which libtacky deleted with the crate, and the Tcl peer that replaced it needs tdom, which this image lacks. Checked in the image: make bundle-appimage (dist/another-im-tauri-0.3.6-x86_64.AppImage, 138,668,536 bytes, carrying libtackymedia_webrtc.so), make check clean, test-ui 130 files 1,644 passed, test-shell 17 binaries 93 passed, test-embed 74 and 6 passed against the built archive, the worker's spike 8, boot 35, omemo 3, host 18, store 64 and page 40 checks passed with test-connect failing on the missing crate, and make (dist/web) built. 2026-10-07 22:31:18 +03:00

anotherim-js

A call in the desktop app

A chat in the desktop app

Another IM: an XMPP messenger with one UI, written in TypeScript and lit, and three ways to run it. Each of them is a host of libtacky's JSON wire API, and none of them speaks XMPP itself: the protocol, the local store, end-to-end encryption, calls and file transfer are libtacky's.

  • The browser client: libtacky compiled to wasm with Emscripten, running in a Web Worker (worker/); the page reaches it over postMessage.
  • The desktop apps (Linux, Windows): a Tauri 2 shell (src-tauri/) that links libtacky.a and runs the backend natively.
  • The Android app: the same Tauri shell built for Android, with a Kotlin half for push, calls and files (src-tauri/gen/android/).

Version 0.4.7, alpha: breaking changes may still be made. Licence MPL-2.0 (LICENSE); the libtacky it embeds is GPL-3.0.

Architecture

                ui/                 TypeScript + lit: views, state, the wire API
                 │                  one Transport interface, three implementations
     ┌───────────┼────────────┐
   mock        worker        tauri
  in-page    libtacky as     libtacky.a in the Tauri shell
  (dev)     wasm (browser)   (desktop, Android)
component language what it is code tests coverage
ui/ TypeScript the client: views/ (lit components, 23,158), state/ (one controller per feature, 13,257), protocol/ (frames, the transports, the in-page mock backend, 5,794), push/ (the service worker, 367), boot and i18n (946) 43,762 29,894 90.3% of lines
src-tauri/ Rust the Tauri shell: the binding over libtacky's tacky.h, the file bridge, D-Bus notices, UnifiedPush, the system tray, the XML console windows, the Android call surface's Rust half 3,640 1,174 95.6% of the lines of its 15 plain-std modules; the binding and the Tauri glue not measured
src-tauri/gen/android/ Kotlin the Android half: push, notifications, the call surface, Save/Share/Open of files, the network watch 2,406 1,582 not measured
worker/ C, JS, Tcl, make the browser backend: Tacky booted under Emscripten behind a postMessage pipe 1,756 1,244 86.9% of the lines of web/
total TypeScript 43,762, Rust 3,651, Kotlin 2,406, JS 907, C 603, make/TOML/Tcl 235 51,564 33,894
everything else Python, JS, sh, make tools/ (the smoke tests, the i18n extractor, the icon scripts), docker/, flatpak/, the Makefiles; not in the total 4,473 -- --

code: lines of code, without blank lines, comments and generated files (the wire API generated from libtacky's schema, ui/src/protocol/generated/api.ts, 10,232 lines, is not counted); the total is the four components', by language, and leaves out everything else. tests: the lines of test code (ui/test/, src-tauri/tests/, the Rust #[cfg(test)] modules, the Kotlin src/test/, worker/test/). Counted on 2026-10-05 at libtacky v1.0.0-alpha3 by the rules of libtacky's tools/stats.py (its file_counts: the files git tracks; Markdown, JSON, CSS, HTML, images and lockfiles are not code, so the seven language catalogues are not counted). coverage: line coverage measured on 2026-10-05 in the image. make coverage-ui runs the UI's tests under V8's coverage (@vitest/coverage-v8) over every ui/src file but the generated wire API: 29,652 of 32,827 lines, 85.2% of branches, 90.8% of functions. make coverage-shell builds the shell's plain-std modules (the ones make test-shell runs) with LLVM's instrumentation: 1,717 of 1,797 lines, from 87.2% (console_windows.rs) to 100%. The worker's own JS was measured once under its seven node suites with node's NODE_V8_COVERAGE: 252 of 290 lines of web/ (tacky-worker.js 93.1%, store.js 84.2%, whose IndexedDB path node cannot run). The Kotlin and the parts of the shell that link libtacky.a have no coverage run.

The language packs: docs/i18n.md. Push and notifications: docs/push.md. Whether a message reaches a closed app: docs/delivery.md. Calls on Android: docs/android-calls.md. How a feature is added, and what each one sends on the wire: docs/porting-from-qt.md.

Supported features

Status, per feature, from the user's point of view:

  • 🔴 TODO: not implemented yet.
  • 🟡 WIP: implemented but not finished (parts missing, a platform without it, known bugs) or not yet tried on the devices it is for.
  • 🔵 MOSTLY: implemented and its tests pass, but not verified by hand against real servers and clients.
  • 🟢 DONE: verified and stabilised. Only a maintainer sets this, by hand.

The statuses come from a code-and-test audit of ui/src, src-tauri and ui/test on 2026-10-01 at 36b7367. The UI's tests run against a fake transport and the in-page mock backend, not against libtacky or a server, so a feature is no further along than the libtacky rows it stands on (libtacky's README, Supported features).

Feature Description Status
Sign in XMPP accounts. The address first and the server's own sign-in page in the browser (OAuth: XEP-0493 on Prosody, ejabberd's own page and X-OAUTH2; the loopback redirect on the desktop, im.another.messenger:/oauth on Android; a browser tab signs in with the password), or, under Sign in with options, username and password on another.im or any other server with its host, port and TLS by hand; the provider changeable, another.im by default; the welcome screen in the theme in force; several accounts, one in front at a time as in Telegram (the account menu lists them with their unread counts and switches between them, the chat list shows the chats of the one in front, and every account's messages notify), each enabled or removed from its menu and disabled on its page; the connection's state and errors on the account. On Android the shell tells libtacky itself when the network comes, goes or is blocked for the app and when the app goes to the background (XEP-0352), so a backgrounded app holds instead of redialling. In a browser the server must offer a WebSocket (RFC 7395). 🔵 MOSTLY
WebSocket discovery The browser's way in. The endpoint is read from the domain's host-meta by libtacky (XEP-0156) or typed under Advanced; the browser path is untested and falls back to wss://<domain>/xmpp-websocket. 🟡 WIP
Sign up In-band registration. The server's form with its own Create account button, CAPTCHA images and proof-of-work (preferred when the server offers both, the picture on request); a server certificate that does not verify is asked about first (in the apps). 🔵 MOSTLY
SSO sign-in Organisation sign-in. Shown only where a broker is configured (the build's SSO_BROKER_URL or the setting); through it or another one, PKCE, then the profile set-up sheet; a build can offer it alone (AUTH_SOURCES=sso). 🔵 MOSTLY
Device link Log in from another device. The device-link code or QR, scanned with the camera or pasted, from the welcome screen's Sign in with a QR code; the two devices' verifying each other is said once libtacky reports it. 🔵 MOSTLY
Chats One-to-one chats. History paging both ways, catch-up from the archive, delivery ticks and read markers, who is typing in the chat header and over the chat list row's preview, per-chat drafts kept and synced, pinned chats, closing a chat (Ctrl+W) until its next message, sort and filter, keyboard navigation. 🔵 MOSTLY
Message actions Reply, edit, delete, react, forward. Replies with a quote and a jump to it, edits of our own messages, deletion (our own, in 1:1 chats), six quick reactions and chips that toggle, forwarding to several chats, multi-select and copy, retry and send without encryption, the stanza's XML. 🔵 MOSTLY
Message styling XEP-0393. Bold, italic, strike, code and quotes drawn from libtacky's tree, behind a switch; the WebKitGTK smoke finds the styled spans in the desktop engine. 🟡 WIP
Search In the chat and across the account. The local store, the server's archive (MAM full text, where offered) or both; hits highlighted and stepped through. 🔵 MOSTLY
Threads XEP-0201. "Reply in thread", the thread pane, the chat's thread list. Files go to the timeline's composer, not the pane's. 🔵 MOSTLY
Saved messages The chat with yourself, always the first row of the chat list, above pinned chats and whatever the sort, and first in the chat picker. 🔵 MOSTLY
Contacts The roster. Contact requests, add, rename and remove, contact details with each online device on a line of its own, the subscription asked for, withdrawn or revoked in either direction, presence and status messages, the last time a contact was active (XEP-0319) deciding their dot and, without a status text, their line -- online (green) under five minutes, while typing or in the chat (XEP-0085), "Last seen 3 hours ago" (yellow) under a day, "Last seen …" (red) after, and "Last seen recently" (between green and yellow) when no time is known -- read from the device they used last, blocking, an optional "contacts first" gate before a stranger can write. 🔵 MOSTLY
Invitations XEP-0379. An invitation link and its QR code; opening one asks, then adds the contact. 🔵 MOSTLY
Find people Directory search and server commands. The server's user directory (XEP-0055) and ad-hoc commands (XEP-0050) with data forms. 🔵 MOSTLY
Gateways Transports. The transports browser, and phone numbers and other foreign addresses resolved to a gateway. Registering with a gateway is only through its ad-hoc commands (libtacky's XEP-0100 row). 🟡 WIP
Rooms Group chats (MUC). Join with room discovery, bookmarks with autojoin, occupants with roles and presence, subject, invitations, kick, ban, roles and memberships, voice requests, destroy, room avatars. 🔵 MOSTLY
OMEMO End-to-end encryption, OMEMO 0.3. Per 1:1 chat, on by default and off by default for a contact that announces no OMEMO device (a transport, say). The padlock and Contact info's single row open the Encryption information window: why the chat is or is not encrypted, their devices and ours with fingerprints, trust and when each was last seen, key verification by QR, removing one of our own devices, Disable behind a confirm and Enable without; a line over the composer says when a chat is not encrypted. Blind trust. A members-only, non-anonymous room has the same padlock, line and window, with its members' devices and who stopped a send that failed; OMEMO 2 is switched off in libtacky in this release. 🟡 WIP
Files Attachments. A tray per chat (pick, paste, drop), HTTP upload (XEP-0363), OMEMO-encrypted files (aesgcm://), thumbnails before the download, automatic download by policy, the thumbnails drawn kept in the account's store so a chat opened again needs no decode or download, a file loaded by hand drawn from the cache on later visits, retry of a failed transfer. 🔵 MOSTLY
Keep attachments decrypted A setting, off by default. Off, an encrypted file is kept as ciphertext and decrypted in memory when shown; on, it is decrypted once when it lands and the plaintext is kept, and turning it either way decrypts or reseals what is already stored. 🔵 MOSTLY
Direct file transfer Jingle (XEP-0234). Device to device over a data channel or in-band, encrypted with JET where the peer offers it; accept, decline, cancel. In the desktop and Android apps only: the browser's backend has no transfer engine. 🔵 MOSTLY
Voice and video messages Record and play. The composer's record button stands in for Send while the field is empty; a tap switches between voice and a round video message, hold records, release sends, slide up locks, slide away cancels. Voice is played in the bubble, a round video loops muted while in view and plays with sound on a tap; recorded in the format other clients play best. Recording and playback on the Linux desktop fixed for WebKitGTK; video messages not yet tried on a phone. 🟡 WIP
Media viewer Pictures, video, sound. A picture opens with one tap, our own sent one included; a full-window viewer (zoom and pan, the engine's player, full screen) and the chat's media browser by month; H.264, HEVC and AAC in the AppImage. Save, Share and Open on Android have not been tried on a phone. 🟡 WIP
Calls in a browser Audio and video, 1:1. The page's own WebRTC under libtacky's Jingle signalling, the DTLS fingerprint sealed with OMEMO and its verdict shown, device pickers (in the call and in Preferences, Calls, with a microphone level and a camera preview to check them), the call minimised to a strip, call history. 🔵 MOSTLY
Calls in the apps Audio only. The desktop and Android apps use libtacky's native media engine and place audio calls, on the microphone and speaker picked in Preferences, Calls (on Android the call's speaker button routes the sound); Android rings over the lock screen with a call notification, not yet tried on a phone. No video here, no group calls anywhere. 🟡 WIP
Notifications While the app runs. One notice per chat as Gajim and Dino show them -- the chat's picture, its name and the message, none while the window has the focus -- withdrawn when read anywhere, a click opens the chat: the service worker in a browser, D-Bus on Linux, the system's toasts on Windows and macOS, Android's own; per-chat mute and notification settings. The unread count (every enabled account's, muted chats left out) is in the title, (N) another.im, and on Linux on the launcher too, over the Unity LauncherEntry signal Ubuntu Dock, Dash to Dock, Dash to Panel and KDE show (plain GNOME Shell's dash needs one of those extensions); see docs/push.md. 🔵 MOSTLY
Push While the app is closed. Web Push in a browser and an installed PWA, UnifiedPush on Linux and Android; a push names who wrote and how many, never what. A muted chat no longer pushes with mod_another_fpush 0.6.0 on the server, and the Android and Linux apps drop a muted chat's push themselves too. Needs another-push-server on the account's server and, outside a browser, a UnifiedPush distributor. 🟡 WIP
Delivery check Will messages arrive while closed? On the first run and in Preferences: push, a background mode (Android's foreground service, the desktop's hide-on-close and autostart) or only while open, with the fix for the platform. 🔵 MOSTLY
App lock A PIN over the app. Set, change and remove in Preferences, a back-off after wrong PINs, relock after time away. No biometrics. 🔵 MOSTLY
Encrypted store The local store's passphrase. Encrypt, decrypt and unlock at start, in the desktop and Android apps; the browser's store has no codec. 🔵 MOSTLY
Certificates A certificate that does not verify. Trust it, ignore failures for 7 days, or give up; a changed pinned certificate announced. In the apps only: in a browser the browser decides. 🔵 MOSTLY
Backup Export and import of the whole installation (XEP-0227), plain or sealed with a passphrase. 🔵 MOSTLY
Appearance Themes, accent and layout. Two themes, Night (the default) and Day -- the chat list in Signal-like white, the chat itself (background, bubbles, composer) in a cool blue -- or the system's light or dark choice (two switches: Follow the system, on by default, and Night mode); an accent colour from eight pastel presets (deepened in Day until they read), the theme's own Signal blue, or Auto, taken from the chat wallpaper's picture (fast-average-color) and adjusted until the words on it read at WCAG AA in either theme; interface scale, chat wallpaper, a resizable list; the system's own face on every platform (the GTK font on Linux, SF on macOS, Segoe UI Variable on Windows, Roboto on Android), with Fira Sans Condensed bundled for the account name in the list header. 🔵 MOSTLY
Translations Seven languages. English, Estonian, Finnish, Latvian, Lithuanian, Russian and Ukrainian; all 1,174 strings translated in each (make i18n-check). 🔵 MOSTLY
Links xmpp: URIs. Opening a room or an invitation asks first; one opened before any account exists waits for the first sign-in. 🔵 MOSTLY
System tray The desktop apps. The app's monochrome glyph with a red dot while anything is unread, Show/Hide, a status menu (online, away, do not disturb, offline) and Quit (Ctrl+Q); "Show icon in the tray" in Preferences, Chats and appearance, on by default wherever something can show the icon (on Linux a StatusNotifierItem host: KDE, or GNOME with the AppIndicator extension) and hidden where nothing can; "Stay connected when closed" (Notifications) hides the window to the tray on close only while the icon is shown, and otherwise closing the window quits the app. Not yet tried on Windows or macOS. 🟡 WIP
Own title bar The Linux desktop app (the Flatpak too). The header bars are the title bar: their empty space moves the window, a double-click maximises (or does what the desktop says), the window buttons are drawn at their ends in the desktop's layout (GNOME's button-layout, read through the XDG Settings portal), and invisible edges resize it. While it floats it is drawn as a libadwaita window, as Nautilus is: 12px rounded corners, a soft shadow and a hairline edge, the window transparent round them and a click on the shadow passing through to what is behind; square and edge to edge maximised, full screen or tiled (the tiled state read from GTK), and square without a compositor. Close goes the window manager's way, so "Stay connected when closed" still applies. "Use the system title bar" in Preferences, Chats and appearance, brings the frame back at once. On macOS the overlay title bar: the system's traffic lights float over the header bars, which leave them room (none in full screen), drag the window and zoom or minimise on a double-click as System Settings says; the same setting gives the standard title bar from the next start. Not yet tried on a Mac. Windows keeps its own frame. 🟡 WIP
Diagnostics Logs. The log level, a log file in the apps, the log view. 🔵 MOSTLY
Connection info and XML console From the account menu. Connection info: the transport, endpoint and how it was found, TLS, SASL, SASL2, FAST, Bind 2, stream management and CSI, the server's software and identities, libtacky's checklist of XEPs and RFCs the server supports, HTTP upload and its limit, push, the contact addresses, our caps and the server's features, all copyable. XML console: the account's traffic one tag a line, filter, direction, pause, with bytes and stanzas counted each way, in a window of its own beside the chat (a second native window on the desktop, a popup in a browser, the sheet when the popup is blocked and on Android). 🟡 WIP
WebXDC, channels, group calls On the experimental branch; not in this release. 🔴 TODO
Stickers XEP-0449. Sticker packs and sending stickers: not started (libtacky's XEP-0449 row is TODO too); a received sticker shows as the picture it shares. 🔴 TODO

Status and tests

Alpha (0.4.7): breaking changes may still be made. Every feature above has tests against a fake transport or the in-page mock backend, and the shell's plain-std modules have unit tests of their own. Nothing has been verified by hand against public servers and other clients as a release check, which is why no row above is DONE.

51,564 lines of code and 33,894 lines of tests (the table under Architecture). Coverage: 90.3% of the UI's lines, 95.6% of the shell's plain-std modules' (make coverage) and 86.9% of the worker's web/; the Kotlin and the binding not measured. The test cases the tree defines:

suite how it runs files cases last run
UI (vitest, happy-dom) make test-ui 145 1,890 2026-10-12, all passed
Types and language catalogues make check (tsc, then make i18n-check) -- -- 2026-10-12, passed
Tauri shell, plain-std modules make test-shell 19 113 2026-10-12, all passed
Tauri shell, the whole crate against libtacky.a make test-embed, after make backend 22 107 2026-10-10 against libtacky v1.0.0-alpha11, all passed (80 built without the desktop feature: 74 in the crate, 6 in tests/embed.rs)
Android, JVM unit tests gradle :app:testUniversalFreeReleaseUnitTest and :app:testUniversalGmsReleaseUnitTest in the android image (the same tests in each flavor) 19 173 2026-10-12, all passed in both
wasm worker, under node make test-worker 5 160 checks 2026-10-12 against libtacky v1.0.0-beta7: boot 35, omemo 3, host 18, store 64 and page 40 passed
WebKitGTK layout smoke make webkit-smoke 1 1,048 checks 2026-10-10, all passed
Chromium smoke of dist/web make web-smoke 1 1 not run for this count

How to use it

Everything builds and tests in this repository's container images; the machine needs only git and podman (or docker). docker/run.sh builds the image a target needs, tags it anotherim-js-build:<profile> and runs the target with this checkout mounted at /src.

git clone --recurse-submodules https://dev.another.im/another-im/anotherim-js.git
cd anotherim-js
make docker-build                 # the backend, dist/web, the web tarball, the AppImage
make test-ui check test-shell     # the tests that need no backend

Every make target runs in the image: on the host, make <goals> only starts the container (docker/run.sh make <goals>, the command line's variables with it), in the android or windows image for those targets. The docker-*, bundle-* and web-image/web-up/web-down targets are not wrapped again: each is already one container launch, all its work in the image. Only help runs on the host. On macOS, which has no build image, every target runs on the Mac instead (see docs/macos.md). The submodules are fetched in the image too, by the targets that need them and only when missing, over public https: the backend from the forge, its third-party pins from GitHub. To push to libtacky over ssh from such a clone, set once git config --global url.ssh://git@dev.another.im:3022/.pushInsteadOf https://dev.another.im/. make dev and make serve are published on the host's loopback; make app opens its window on the host's display, keeping its data under build/app-home. HOST=1 runs a target on the host anyway, for a machine set up to build without containers.

platform target output status
Browser make docker-build (in the image: make web-tar) dist/web/ and dist/anotherim-js-<version>.tar.gz, a static tree any server can host 🔵 MOSTLY
Linux AppImage make docker-build or make bundle-appimage dist/another-im-tauri-<version>-x86_64.AppImage 🔵 MOSTLY
Linux Flatpak make bundle-flatpak (the flatpak image, run privileged; the GNOME 50 runtime) dist/another-im-tauri-<version>-x86_64.flatpak 🟡 WIP
Windows make docker-windows (with a smoke run under wine) or make bundle-windows dist/another-im-tauri-<version>-x86_64-setup.exe (NSIS) 🟡 WIP: cross-built and run under wine, never on Windows
Android make docker-android or make bundle-android dist/another-im-tauri-<version>[-unsigned].apk (arm64-v8a, Android 11 and later), the free build: UnifiedPush, no Google code 🟡 WIP: built, parts not yet tried on a phone
Android with FCM make bundle-android-gms ANOTHER_IM_GOOGLE_SERVICES=<google-services.json> dist/another-im-tauri-<version>-gms[-unsigned].apk, the gms build: FCM where no UnifiedPush distributor is installed (docs/push.md) 🟡 WIP: built, FCM not yet tried on a phone
macOS make bundle-macos, on a Mac only, see docs/macos.md src-tauri/target/aarch64-apple-darwin/release/bundle/: .app and .dmg (unsigned) 🟡 WIP: built on Apple silicon, the app starts; sign-in and calls not yet tried

The apk is signed when ANOTHER_QT_ANDROID_KEYSTORE, ANOTHER_QT_ANDROID_KEY_ALIAS and ANOTHER_QT_ANDROID_KEYSTORE_PASS are set, unsigned otherwise; ANOTHER_IM_ANDROID_DEBUG=1 makes gradle's debug apk.

The app's ID is im.another.messenger on every platform; up to 0.4.6 it was im.another.desktop. On the desktop nothing is lost: the first start of a newer build moves the old ID's directories to the new names before it opens anything (src-tauri/src/id_migrate.rs: the XDG data, config and cache directories and the autostart entry on Linux, the whole old sandbox in the Flatpak, %APPDATA% and %LOCALAPPDATA% on Windows, Application Support, Caches and WebKit on macOS), and a directory already there under the new name is left as it is. The Flatpak installs as a new app beside the old one, which is then empty and can be uninstalled. On Android the ID is the package name, and Android has no way to rename a package: the new apk is a new app that does not upgrade the old one. Testers install it beside the old app, log in again (the old app's store, accounts and keys stay in the old app), and uninstall the old one when they no longer need it; adb commands name the new package (adb logcat --pid=$(adb shell pidof im.another.messenger)). JOBS=<n> caps the parallelism. make docker-shell (PROFILE=android or PROFILE=windows for those images) is a shell where each target below runs as it is:

target what it does
make, make web the browser client, dist/web (the wasm backend and the UI)
make web-tar dist/web as a tarball
make backend libtacky.a, the libwebrtc media backend and the wasm backend, through libtacky's own make, every tree under build/backend
make test test-ui, test-shell, test-worker, and test-embed when libtacky.a is built
make check tsc --noEmit, then make i18n-check (STRICT=1: a missing translation fails too)
make webkit-smoke ui/dist drawn in WebKitGTK, the desktop's engine, screen by screen (SMOKE_LANG=uk: in a language pack)
make web-smoke dist/web booted in headless Chromium (CHROME=)
make dev local web development on http://localhost:5173: the UI with the real wasm backend, the UI hot-reloaded and the worker rebuilt and the page reloaded when the backend's sources change (DEV_BACKEND=mock: the in-page mock, nothing built; DEV_POLL=1 where file events do not cross the mount)
make web-serve dist/web on http://localhost:8000
make app the desktop window, debug, after make backend
make help every target

Two build knobs shape the client: AUTH_SOURCES (the welcome screen's ways in, xmpp and sso, both by default; SSO_BROKER_URL names the broker, without which the organisation button is hidden) and MODULES (the optional areas calls, transports, threads, backup and lock, all by default). A module left out is neither registered nor bundled.

On the wire the client is another.im (Js), its XEP-0030 identity web, pc or phone. The apps keep their profile under ~/.config/libtacky/another-im-js and its XDG siblings (the platform's equivalents on Windows and macOS, the app's private storage on Android), the browser in its Origin Private File System, files used in place (IndexedDB in a browser without OPFS sync access handles; a store kept there by an earlier build is moved into OPFS once). Two apps cannot hold one profile: the second refuses to start and names the first, as a second browser tab does. --config-dir, --data-dir, --cache-dir, --debug-level and --debug-file are passed on to the backend, and ANOTHERIM_DEBUG_LEVEL and ANOTHERIM_DEBUG_FILE stand in for the last two where there is no command line. In the apps every HTTP request (uploads, downloads, the SSO token) is libtacky's own; in a browser tab it is the browser's.

Serving the browser client (web.another.im)

The browser client is a static tree. On the server (git and podman or docker are all it needs):

git clone --recurse-submodules https://dev.another.im/another-im/anotherim-js.git
cd anotherim-js
make web-up        # builds dist/web in the build image, wraps it in nginx, runs it on 127.0.0.1:8080 (WEB_BIND, WEB_PORT)

make web-up builds the image anotherim-js-web (docker/web-image.Dockerfile: nginx with dist/web, the Content-Security-Policy and cache headers the client needs, /healthz) and (re)starts the container anotherim-js on 127.0.0.1:8080 (WEB_PORT= for another), restarting unless stopped. The front proxy on the machine terminates TLS and forwards the public name to it, for nginx:

server {
    listen 443 ssl; listen [::]:443 ssl; http2 on;
    server_name web.another.im;
    # ssl_certificate ... / ssl_certificate_key ...
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
    }
}

It must be https: a browser allows the local store, the service worker and the camera and microphone on a secure origin only. To update: git pull && git submodule update --init --recursive && make web-up. make web-down stops it. To have systemd run it instead (started at boot, restarted when it stops), make systemd-unit prints the unit for the engine in use: a Quadlet .container for podman, a .service running the container in the foreground for docker, with the engine's path and the WEB_* variables (WEB_NAME, WEB_IMAGE, WEB_BIND, WEB_PORT) filled in:

sudo loginctl enable-linger $USER   # once, rootless: the user's services start at boot
make web-image                    # the image the unit runs
# podman:
mkdir -p ~/.config/containers/systemd
make systemd-unit > ~/.config/containers/systemd/anotherim-js.container
# docker (rootless; /etc/systemd/system/ for the system daemon):
mkdir -p ~/.config/systemd/user
make systemd-unit > ~/.config/systemd/user/anotherim-js.service
systemctl --user daemon-reload && systemctl --user restart anotherim-js

After a git pull, make web-image and the restart again; systemctl --user status anotherim-js and journalctl --user -u anotherim-js show it. As root the unit goes in /etc/containers/systemd/ (podman) or /etc/systemd/system/ (docker) and systemctl runs without --user. The client finds the XMPP WebSocket through the domain's host-meta, else at wss://<domain>/xmpp-websocket.

make serve is the quick look instead: the same tree from a python server on 127.0.0.1:8000, in the foreground.

The backend

libtacky is the submodule libtacky/ (https://dev.another.im/another-im/libtacky.git, given relative in .gitmodules so it follows however this repository was cloned), pinned by this repository and tracking libtacky's develop branch; a recursive clone brings its third-party submodules too. The backend is looked for in this order: TACKY_ROOT=<dir> (or TACKY=<dir> to make), ../libtacky beside this checkout, ../another-tacky (its old name), then the nested libtacky/. To move the pin to the tip of develop:

git submodule update --remote libtacky && git add libtacky

A clone made while the submodule was still at tacky/ (before 2026-10-02) moves over with:

git pull
rm -rf tacky .git/modules/tacky
git submodule sync --recursive && git submodule update --init --recursive

Licence

anotherim-js is MPL-2.0 (LICENSE). libtacky, which every build embeds (the wasm worker, the Tauri shells), is GPL-3.0, so a build is distributed under the GPL-3.0 terms as a whole; MPL-2.0 is compatible with that. The bundled typeface, Fira Sans Condensed (ui/public/fonts/), is © 2012-2015 the Mozilla Foundation and Telefonica S.A. under the SIL Open Font License 1.1 (ui/public/fonts/OFL.txt). The icons are Lucide's (https://lucide.dev), © Lucide Icons and Contributors under the ISC licence, some derived from Feather (© Cole Bemis, MIT); both texts ship as ui/public/licenses/lucide.txt, and tools/icons-from-lucide.mjs regenerates ui/src/views/icons.ts from the pinned release. The AppImage carries FFmpeg's decoders (LGPL-2.1+) for H.264, HEVC and AAC, which are subject to patents in some jurisdictions.