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.
96 lines
2.8 KiB
Swift
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
|
|
}
|
|
}
|
|
|