Files
Luma/Sources/Shared/UI/EncryptionDevicesView.swift
T
2026-07-30 20:07:51 +07:00

150 lines
5.4 KiB
Swift

import SwiftUI
struct EncryptionDevicesView: View {
@ObservedObject var model: AppModel
let conversation: Conversation
@Environment(\.dismiss) private var dismiss
@State private var devices: [OMEMODevice] = []
var body: some View {
NavigationStack {
List {
Section {
Label(
statusTitle,
systemImage: statusIcon
)
.foregroundStyle(statusColor)
} footer: {
Text("Luma использует trust-on-first-use. Сверьте отпечаток по другому каналу и отметьте устройство как проверенное.")
}
if devices.isEmpty {
Section("Устройства контакта") {
ContentUnavailableView(
"Ключи ещё не получены",
systemImage: "key.horizontal",
description: Text("Они появятся после первой попытки отправки или получения OMEMO-сообщения.")
)
}
} else {
Section("Устройства контакта") {
ForEach(devices) { device in
DeviceRow(device: device) {
model.setDeviceVerified(
device.trust != .verified,
jid: device.jid,
deviceID: device.deviceID
)
reload()
}
}
}
}
if let fingerprint = model.ownFingerprint {
Section("Это устройство") {
Text(fingerprint.chunked(every: 8).joined(separator: " "))
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
}
}
}
.navigationTitle("Шифрование")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Готово") { dismiss() }
}
}
.onAppear(perform: reload)
}
#if os(macOS)
.frame(minWidth: 480, minHeight: 500)
#endif
}
private func reload() {
devices = model.devices(for: conversation.jid).filter { !$0.isOwn }
}
private var statusTitle: String {
guard model.encryptionEnabled(for: conversation.jid) else { return "OMEMO выключено для этого чата" }
return model.isOMEMOReady ? "OMEMO активно" : "OMEMO подготавливается"
}
private var statusIcon: String {
guard model.encryptionEnabled(for: conversation.jid) else { return "lock.open" }
return model.isOMEMOReady ? "lock.shield.fill" : "lock.rotation"
}
private var statusColor: Color {
model.encryptionEnabled(for: conversation.jid) && model.isOMEMOReady ? .green : .orange
}
}
private struct DeviceRow: View {
let device: OMEMODevice
let toggleVerification: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("Устройство \(device.deviceID)", systemImage: device.isActive ? "iphone" : "iphone.slash")
.font(.headline)
Spacer()
Button(action: toggleVerification) {
Label(trustTitle, systemImage: trustIcon)
}
.buttonStyle(.bordered)
.tint(trustColor)
}
Text(device.formattedFingerprint)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
.padding(.vertical, 5)
}
private var trustTitle: String {
switch device.trust {
case .verified: return "Проверено"
case .trusted: return "Доверено"
case .undecided: return "Проверить"
case .compromised: return "Скомпрометировано"
}
}
private var trustIcon: String {
switch device.trust {
case .verified: return "checkmark.seal.fill"
case .trusted: return "checkmark.shield"
case .undecided: return "questionmark.diamond"
case .compromised: return "exclamationmark.triangle.fill"
}
}
private var trustColor: Color {
switch device.trust {
case .verified: return .green
case .trusted: return .blue
case .undecided: return .orange
case .compromised: return .red
}
}
}
private extension String {
func chunked(every size: Int) -> [String] {
guard size > 0 else { return [self] }
var result: [String] = []
var cursor = startIndex
while cursor < endIndex {
let end = index(cursor, offsetBy: size, limitedBy: endIndex) ?? endIndex
result.append(String(self[cursor..<end]))
cursor = end
}
return result
}
}