fix MAM history loading, multi-device resource, and OMEMO self-session
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

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.
This commit is contained in:
wt committed 2026-08-18 02:41:52 +07:00
1 parent 0b4a552044
commit 9c8cff7168
9 files changed
+235 -141

No files matched your search

+25 -47
View File
@@ -334,27 +334,13 @@ struct ChatView: View {
Color.clear
.frame(height: 1)
.accessibilityHidden(true)
// .onAppear {
//// guard !model.isLoadingOlderHistory else { return }
// guard historyTopTriggerArmed,
// !model.isLoadingOlderHistory
// else {
// return
// }
//
// historyTopTriggerArmed = false
// historyLoadAnchorID = timelineEntries.first?.id
// model.loadOlderHistoryForSelectedConversation()
// }
.background {
GeometryReader { geometry in
Color.clear.preference(
key: TimelineTopYPreferenceKey.self,
value: geometry.frame(
in: .named(Self.timelineCoordinateSpace)
).minY
)
}
.onAppear {
guard historyTopTriggerArmed,
!model.isLoadingOlderHistory
else { return }
historyTopTriggerArmed = false
historyLoadAnchorID = timelineEntries.first?.id
model.loadOlderHistoryForSelectedConversation()
}
.onDisappear {
historyTopTriggerArmed = true
@@ -374,6 +360,14 @@ struct ChatView: View {
description: Text(emptyChatDescription)
)
.padding(.top, 80)
.onAppear {
// Empty timeline has no scroll gesture, so load
// the first page directly when it appears.
guard model.hasMoreOlderHistory,
!model.isLoadingOlderHistory
else { return }
model.loadOlderHistoryForSelectedConversation()
}
} else {
ForEach(timelineEntries) { entry in
let message = entry.message
@@ -441,22 +435,6 @@ struct ChatView: View {
)
}
#endif
.onPreferenceChange(TimelineTopYPreferenceKey.self) { topY in
if topY > 24 {
historyTopTriggerArmed = true
return
}
guard hasCompletedInitialScroll,
!timelineEntries.isEmpty,
model.hasMoreOlderHistory,
!model.isLoadingOlderHistory,
historyTopTriggerArmed
// topY <= 24
else { return }
historyTopTriggerArmed = false
historyLoadAnchorID = timelineEntries.first?.id
model.loadOlderHistoryForSelectedConversation()
}
.task(id: timelineEntries.last?.id) {
await performInitialScrollAfterLayout(using: proxy)
}
@@ -1571,8 +1549,16 @@ struct ChatView: View {
}
private func performInitialScrollAfterLayout(using proxy: ScrollViewProxy) async {
guard !hasCompletedInitialScroll,
!model.selectedMessages.isEmpty else { return }
guard !hasCompletedInitialScroll else { return }
guard !model.selectedMessages.isEmpty else {
// Empty conversation: there is nothing to scroll to, so the
// scroll-to-top gesture can never fire. Load the first page of
// history directly so a newly-created or emptied chat still fills.
if model.hasMoreOlderHistory, !model.isLoadingOlderHistory {
model.loadOlderHistoryForSelectedConversation()
}
return
}
// ScrollViewReader can receive its first command before a long
// LazyVStack has published the bottom anchor. Yield once for layout,
@@ -1614,14 +1600,6 @@ private struct TimelineBottomYPreferenceKey: PreferenceKey {
}
}
private struct TimelineTopYPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .greatestFiniteMagnitude
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
#if os(iOS)
/// Reads the real UIScrollView offset without replacing its delegate or
/// installing another pan recognizer. SwiftUI geometry preferences can stop