Files
Luma/Sources/Shared/Models/AccountConfiguration.swift
T
wt 9c8cff7168
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
fix MAM history loading, multi-device resource, and OMEMO self-session
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.
2026-08-18 02:41:52 +07:00

96 lines
2.8 KiB
Swift

import Foundation
struct AccountConfiguration: Codable, Equatable, Hashable, Identifiable, Sendable {
var jid: String
var displayName: String
var resource: String
var manualHost: String?
var manualPort: Int?
var usesDirectTLS: Bool
var id: String { normalizedJID }
var normalizedJID: String {
jid.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
var domain: String? {
let pieces = normalizedJID.split(separator: "@", omittingEmptySubsequences: false)
guard pieces.count == 2, !pieces[1].isEmpty else { return nil }
return String(pieces[1])
}
var effectiveResource: String {
let trimmed = resource.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty || trimmed == DeviceResource.legacyDefault {
return DeviceResource.default
}
return trimmed
}
init(
jid: String,
displayName: String = "",
resource: String = "Luma",
manualHost: String? = nil,
manualPort: Int? = nil,
usesDirectTLS: Bool = false
) {
self.jid = jid
self.displayName = displayName
self.resource = resource
self.manualHost = manualHost?.nilIfBlank
self.manualPort = manualPort
self.usesDirectTLS = usesDirectTLS
}
func validated() throws -> AccountConfiguration {
let normalized = normalizedJID
let pieces = normalized.split(separator: "@", omittingEmptySubsequences: false)
guard pieces.count == 2,
!pieces[0].isEmpty,
!pieces[1].isEmpty,
!normalized.contains(where: { $0.isWhitespace }) else {
throw AccountValidationError.invalidJID
}
if let manualHost, manualHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
throw AccountValidationError.invalidHost
}
if let manualPort, !(1...65_535).contains(manualPort) {
throw AccountValidationError.invalidPort
}
var copy = self
copy.jid = normalized
copy.manualHost = manualHost?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfBlank
copy.resource = effectiveResource
return copy
}
}
enum AccountValidationError: LocalizedError {
case invalidJID
case invalidHost
case invalidPort
var errorDescription: String? {
switch self {
case .invalidJID:
return "Введите полный JID в формате user@example.org."
case .invalidHost:
return "Адрес сервера не может быть пустым."
case .invalidPort:
return "Порт должен быть числом от 1 до 65535."
}
}
}
private extension String {
var nilIfBlank: String? {
isEmpty ? nil : self
}
}