- Replace the sidebar-based MainChatView with MainTabView: a custom
bottom bar with Контакты / Звонки / Чаты / Настройки that looks the
same on iOS and macOS, and open chats via push navigation.
- Put the search field above the chat and contact lists (searchable on
iOS, an inline field on macOS) and add an aggregated call-history
screen backed by model.callHistoryMessages.
- Embed SettingsView as a tab, update verify.sh guards, and keep CI on
deterministic unit tests while the UI tests stay runnable manually.
- Lock only when the scene really backgrounds: transient inactive
states (system photo picker, app switcher) no longer swap in the
lock screen and break gallery selection on iOS.
- Cache biometric availability once per session off the main thread
instead of calling LAContext.canEvaluatePolicy in view bodies,
which froze the macOS lock screen, and auto-prompt biometrics on
iOS only.
- Keep UI tests as an opt-in target and guard both fixes in
verify.sh.
- Skip older-history loads silently while the connection is down:
the trigger previously failed with "not connected", popped the
global alert over the timeline and froze scrolling until dismissed.
- Accept fast reply-swipe flicks whose end state is clearly leftward
even without an intermediate horizontal lock, while scroll-owned
touches still never activate the swipe.
- Add a seeded-chat UI test mode (launch argument), accessibility
identifiers and a LumaUITests target with timeline scroll and reply
swipe tests; guard the fix in verify.sh.
- Accept only negative-width translations in canLock, offset and
shouldReply so swiping left-to-right stays entirely with the scroll
view while right-to-left keeps the lock/follow behaviour.
- Mirror the restriction in MessageReplySwipeTests and fix the
line-sensitive PhotosPicker data-fallback guard in verify.sh.
- Lock the gesture into a reply swipe once it starts clearly
horizontal (1.6 dominance, 12pt) and relax the ratio to 1.05
afterwards, so a slightly diagonal finish no longer cancels the
swipe while vertical scrolls stay untouched.
- Restyle the composer reply banner as a rounded telegram-like card
with the reply header, close button and two-line quote preview.
- Cover the lock/follow behaviour in MessageReplySwipeTests.
- Raise the swipe dominance threshold to 2.0 and add a 24pt
activation distance so diagonal timeline scrolls never move bubbles
or fire haptics, and snap the indicator back when a gesture turns
vertical mid-drag.
- Cover the stricter thresholds in MessageReplySwipeTests.
- After a call ends, send a plaintext <call-history/> service message
to the user's own bare JID with direction, status, duration, start
time, peer and video flag plus the call id as origin-id.
- Intercept the payload on live, carbon and MAM paths and upsert the
same system call card on every device; deduplication reuses the
origin-id as clientID, and missed incoming calls bump unread.
- Cover payload parsing and round-trip in CallHistorySyncTests, guard
the namespace and interception in verify.sh, and document the flow
in ARCHITECTURE.md.
- Gate every layer behind AppLockView when the lock is enabled and
lock on launch and on backgrounding; the passcode lives only in the
Keychain (AppLockVault) and the enable state defaults to off.
- Add the lock section to Settings: enable/setup, change passcode and
disable all require the current passcode via a shared passcode
sheet; Face ID / Touch ID unlock prompts automatically and can be
toggled with passcode confirmation.
- Add NSFaceIDUsageDescription to both app targets, cover the passcode
policy with tests, guard the feature in verify.sh and document it in
SECURITY.md.
- Delete a group chat from GroupInfoView or via a swipe in the chat
list: remove the conversation and its messages from SwiftData,
clean per-chat state, and leave the room on the server first when
joined.
- Keep "Покинуть комнату" as leave-without-deleting and suppress
deleted rooms for the session so roomState events cannot recreate
them; an explicit rejoin or a fresh invitation clears the
suppression.
- Add verify.sh invariants and document the behaviour in
ARCHITECTURE.md.
- Drive chat list, timeline and forward picker from SwiftData @Query
instead of AppModel's in-memory arrays; inject the per-account
ModelContext from RootView with an in-memory fallback.
- Physically delete locally deleted messages (context.delete) and
purge rows marked deleted by older builds on store open, so @Query
views never resurrect them.
- Delete the legacy JSON snapshot only after a successful import save
and restore the completeUntilFirstUserAuthentication data protection
attribute on the store file.
- Mirror the old ChatArchive tolerant decoding in LegacyArchiveImporter
so snapshots from older schema versions (missing reactions,
isGroupMessage, roster fields) still import.
- Cover the new behaviour in ArchiveStoreTests and guard it with new
verify.sh invariants; update AGENTS.md, ARCHITECTURE.md and
SECURITY.md.
Models:
- Convert Conversation and ChatMessage to SwiftData @Model classes with
a
one-to-many relationship and cascade delete.
- Rename ChatMessage.id to clientID and drop Conversation.id in favor of
jid
(the string id would clash with PersistentIdentifier).
- Keep MessageReaction as a Codable value stored in an array attribute.
Persistence:
- Add ArchiveStore, a per-account ModelContainer/ModelContext that
replaces
the JSON ChatArchive with load/save/erase.
- Add ArchiveMetadataRecord for the durable MAM checkpoints, cursor,
locally
deleted message IDs and roster contact JIDs.
- Add LegacyArchiveImporter to import the old JSON snapshot once and
delete
the file afterwards, preserving existing history.
AppModel:
- Replace ChatArchive with ArchiveStore and persist via context.save();
insert new models and delete the replaced object on upsert merges so
SwiftData never keeps an orphaned duplicate.
Tests:
- Replace ChatArchiveTests with ArchiveStoreTests (store round-trip and
legacy import) and drop the now-obsolete Codable round-trip
assertions.
Server information (Monal-style):
- Add ServerInformation model and ServerInfoView reachable from
Settings,
showing server software (XEP-0092), disco#info identities and
features,
conference (MUC) services via disco#items, and STUN/TURN via XEP-0215.
- Add a curated XEP capability list with per-XEP success/error status
derived
from server/account disco features and connection flags.
- Detect PEP (0163) from account disco pubsub#* features, HTTP Upload
(0363)
from the main domain plus its components, and add Roster Versioning
(0237),
Pre-Authenticated Roster Subscription (0379), and SASL SCRAM Downgrade
Protection (0474, neutral because Martin does not expose SSDP).
Connection statistics:
- Add LumaConnectionStatsModule, an XmppStanzaFilter registered before
stream
management, to count sent / acknowledged / received stanzas.
- Track last login and SMACKS session timestamps, and read SASL
mechanisms
and Client State Indication from the raw stream features.
MAM / OMEMO handling:
- Prefer a plaintext <body> fallback when OMEMO decryption fails instead
of
showing "failed to decrypt".
- Keep the optimistic text of our own outgoing message when its echo
cannot
be decrypted back, instead of replacing it with a placeholder.
Emoji picker:
- Add EmojiCatalog and a cross-platform EmojiPickerView.
- Open the picker for reactions (replacing the static quick list) and
from
the composer's smiley button, inserting the chosen emoji into the
draft.
Multi-device:
- Add DeviceResource with a stable per-install UUID-based resource
(Luma-<hex>) persisted in UserDefaults and migrated from legacy
"Luma".
- AccountConfiguration.effectiveResource falls back to
DeviceResource.default
for a blank or legacy resource; LoginView defaults resource to "" with
an
auto placeholder.
- Prevents two devices from sharing the same full JID resource and
kicking
each other off the stream (the "xmpp stream error" conflict).
MAM / history:
- loadOlderHistory now always issues the RSM page request instead of
deferring, so scrolling up reliably loads older messages.
- Track interactive history mutations separately so live messages are
not
dropped while an older-history page is decoding.
- ChatView: replace the unreliable GeometryReader/preference scroll
trigger
with onAppear/onDisappear on a top sentinel; auto-load the first page
when
the conversation is empty.
- AppModel: reset hasMoreOlderHistory for empty conversations and clear
isLoadingOlderHistory on disconnect.
- Decode own/duplicate/notEncrypted messages to
decryption-failed/plaintext
placeholders instead of dropping them, so every MAM stanza is
collected.
OMEMO:
- Remove the encrypt-to-self setup that built a Signal session with the
local
device (cryptographically invalid), which caused an endless "Bad MAC"
loop
and a stream of undecryptable messages in the self-chat.
- Add LumaOMEMOStore.removeSessionWithOwnDevice() and call it on connect
to
purge any self-session persisted by the previous build.
Tests:
- Update AccountConfigurationTests for the unique resource and add
coverage
for legacy-resource migration and explicit-resource preservation.
thread
- Replace O(n^2) origin-id/stanza-id lookups in AppModel with hash
indexes
and stop rebuilding the whole index on every unseen stanza-id, so
archive
application no longer grows quadratically with history size.
- Run OMEMO decryption on a serial background queue instead of the main
actor, and decrypt archived stanzas in small batches with a shorter
yield,
keeping the UI responsive during large archive catch-ups.
- Show the "Синхронизация истории…" banner only for the one-page
bootstrap
window; incremental backlog catch-up continues silently in the
background.
- Fix MUC-MAM: publish decoded group mutations at the end of room
catch-up;
they were previously accumulated and then silently dropped.
- Retry a failed catch-up pass automatically (bounded) and lengthen the
query/apply timeouts so a single slow page no longer leaves history
unloaded until the next app activation.