- Wait until the picker movie's file size stops changing before
staging it, so a movie whose final bytes are still being flushed is
never copied truncated (which produced drafts without a thumbnail
and unplayable previews).
- Log the whole camera video path (pickup size, staging result,
analyze outcome, thumbnail decode) under the video-preview os_log
category for fast on-device diagnosis.
- Cover the video analysis pipeline with a simulator test that
generates an HEVC QuickTime movie and asserts duration plus JPEG
thumbnail, and guard the diagnostics in Scripts/verify.sh.
- Normalize SCRAM passwords with RFC 4013 SASLprep before the
challenge response, so they match SASLprep-compliant servers
(Martin hashes raw UTF-8) and PLAIN still sends the password
untouched.
- Capture the raw <failure/> condition with LumaSaslFailureModule and
map SASL errors to actionable Russian messages instead of the
cryptic OS-localized "Martin.SaslError, error 5".
- Cover SASLprep and failure message mapping with unit tests and new
verify.sh invariants.
- 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.
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.