Delete history, allow retry

This commit is contained in:
Christoph Hagen
2023-08-14 10:39:29 +02:00
parent 95ece1ddcc
commit 7a443d51b3
26 changed files with 440 additions and 220 deletions

View File

@ -0,0 +1,10 @@
import Foundation
enum ConnectionStrategy: String, CaseIterable, Identifiable {
case local = "Local"
case localFirst = "Local first"
case remote = "Remote"
case remoteFirst = "Remote first"
var id: Self { self }
}

View File

@ -1,14 +1,22 @@
import Foundation
import CBORCoding
protocol HistoryManagerProtocol {
func loadEntries() -> [HistoryItem]
func save(item: HistoryItem) throws
class HistoryManagerBase: ObservableObject {
@Published
var entries: [HistoryItem] = []
}
final class HistoryManager: HistoryManagerProtocol {
protocol HistoryManagerProtocol: HistoryManagerBase {
var entries: [HistoryItem] { get }
func save(item: HistoryItem) throws
func delete(item: HistoryItem) -> Bool
}
final class HistoryManager: HistoryManagerBase, HistoryManagerProtocol {
private let encoder = CBOREncoder(dateEncodingStrategy: .secondsSince1970)
@ -25,11 +33,20 @@ final class HistoryManager: HistoryManagerProtocol {
private let fileUrl: URL
init() {
override init() {
self.fileUrl = HistoryManager.documentDirectory.appendingPathComponent("history2.bin")
super.init()
Task {
print("Loading history...")
let all = loadEntries()
DispatchQueue.main.async {
self.entries = all
print("History loaded (\(self.entries.count) entries)")
}
}
}
func loadEntries() -> [HistoryItem] {
private func loadEntries() -> [HistoryItem] {
guard fm.fileExists(atPath: fileUrl.path) else {
print("No history data found")
return []
@ -65,8 +82,7 @@ final class HistoryManager: HistoryManagerProtocol {
}
func save(item: HistoryItem) throws {
let entryData = try encoder.encode(item)
let data = Data([UInt8(entryData.count)]) + entryData
let data = try convertForStorage(item)
guard fm.fileExists(atPath: fileUrl.path) else {
try data.write(to: fileUrl)
print("First history item written (\(data[0]))")
@ -78,15 +94,50 @@ final class HistoryManager: HistoryManagerProtocol {
try handle.close()
print("History item written (\(data[0]))")
}
@discardableResult
func delete(item: HistoryItem) -> Bool {
let newItems = entries
.filter { $0 != item }
let data: FlattenSequence<[Data]>
do {
data = try newItems
.map(convertForStorage)
.joined()
} catch {
print("Failed to encode items: \(error)")
return false
}
do {
try Data(data).write(to: fileUrl)
} catch {
print("Failed to save items: \(error)")
return false
}
entries = newItems
return true
}
private func convertForStorage(_ item: HistoryItem) throws -> Data {
let entryData = try encoder.encode(item)
return Data([UInt8(entryData.count)]) + entryData
}
}
final class HistoryManagerMock: HistoryManagerProtocol {
func loadEntries() -> [HistoryItem] {
[.mock]
final class HistoryManagerMock: HistoryManagerBase, HistoryManagerProtocol {
override init() {
super.init()
self.entries = [.mock]
}
func save(item: HistoryItem) throws {
entries.append(item)
}
func delete(item: HistoryItem) -> Bool {
entries = entries.filter { $0 != item }
return true
}
}

View File

@ -2,6 +2,15 @@ import Foundation
import CryptoKit
import SwiftUI
struct KeySet {
let remote: SymmetricKey
let device: SymmetricKey
let server: Data
}
extension KeyManagement {
enum KeyType: String, Identifiable, CaseIterable {
@ -82,6 +91,9 @@ private struct KeyChain {
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != -25300 else {
return nil
}
guard status == errSecSuccess else {
print("Failed to get \(type): \(status)")
return nil
@ -137,6 +149,15 @@ final class KeyManagement: ObservableObject {
}
}
func getAllKeys() -> KeySet? {
guard let remoteKey = get(.remoteKey),
let token = get(.authToken)?.data,
let deviceKey = get(.deviceKey) else {
return nil
}
return .init(remote: remoteKey, device: deviceKey, server: token)
}
func get(_ type: KeyType) -> SymmetricKey? {
keyChain.load(type)
}

View File

@ -21,7 +21,7 @@ struct ContentView: View {
@AppStorage("deviceID")
private var deviceID: Int = 0
@State
@ObservedObject
var keyManager = KeyManagement()
let history = HistoryManager()
@ -133,7 +133,7 @@ struct ContentView: View {
.animation(.easeInOut, value: state.color)
.sheet(isPresented: $showSettingsSheet) {
SettingsView(
keyManager: $keyManager,
keyManager: keyManager,
serverAddress: $serverPath,
localAddress: $localAddress,
deviceID: $deviceID,
@ -142,7 +142,7 @@ struct ContentView: View {
useLocalConnection: $useLocalConnection)
}
.sheet(isPresented: $showHistorySheet) {
HistoryView(manager: history)
HistoryView(history: history)
}
}
.preferredColorScheme(.dark)

View File

@ -0,0 +1,14 @@
import Foundation
extension Array {
func count(where closure: (Element) -> Bool) -> Int {
var result = 0
forEach { element in
if closure(element) {
result += 1
}
}
return result
}
}

View File

@ -3,7 +3,6 @@ import Foundation
struct HistoryItem {
/// The sent/received date (local time, not including compensation offset)
let requestDate: Date
@ -11,46 +10,25 @@ struct HistoryItem {
let usedLocalConnection: Bool
let response: ClientState?
var response: ClientState
let responseMessage: Message.Content?
let responseDate: Date?
let responseDate: Date
init(sent message: Message.Content, date: Date, local: Bool) {
self.requestDate = date
init(sent message: Message.Content, sentDate: Date, local: Bool, response: ClientState, responseDate: Date, responseMessage: Message.Content?) {
self.requestDate = sentDate
self.request = message
self.responseMessage = nil
self.response = nil
self.responseDate = nil
self.usedLocalConnection = local
}
func didReceive(response: ClientState, date: Date?, message: Message.Content?) -> HistoryItem {
.init(sent: self, response: response, date: date, message: message)
}
func invalidated() -> HistoryItem {
didReceive(response: .responseRejected(.invalidAuthentication), date: responseDate, message: responseMessage)
}
func notAuthenticated() -> HistoryItem {
didReceive(response: .responseRejected(.missingKey), date: responseDate, message: responseMessage)
}
private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message.Content?) {
self.requestDate = sent.requestDate
self.request = sent.request
self.responseDate = date
self.responseMessage = message
self.responseMessage = responseMessage
self.response = response
self.usedLocalConnection = sent.usedLocalConnection
self.responseDate = responseDate
self.usedLocalConnection = local
}
// MARK: Statistics
var roundTripTime: TimeInterval? {
responseDate?.timeIntervalSince(requestDate)
var roundTripTime: TimeInterval {
responseDate.timeIntervalSince(requestDate)
}
var deviceTime: Date? {
@ -68,14 +46,14 @@ struct HistoryItem {
guard let deviceTime = deviceTime else {
return nil
}
return responseDate?.timeIntervalSince(deviceTime)
return responseDate.timeIntervalSince(deviceTime)
}
var clockOffset: Int? {
guard let interval = roundTripTime, let deviceTime = deviceTime else {
guard let deviceTime = deviceTime else {
return nil
}
let estimatedArrival = requestDate.advanced(by: interval / 2)
let estimatedArrival = requestDate.advanced(by: roundTripTime / 2)
return Int(deviceTime.timeIntervalSince(estimatedArrival))
}
@ -125,7 +103,12 @@ extension HistoryItem {
static var mock: HistoryItem {
let content = Message.Content(time: Date.now.timestamp, id: 123, device: 0)
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0)
return .init(sent: content, date: .now, local: false)
.didReceive(response: .openSesame, date: .now + 2, message: content2)
return .init(
sent: content,
sentDate: .now,
local: false,
response: .openSesame,
responseDate: .now + 2,
responseMessage: content2)
}
}

View File

@ -16,11 +16,8 @@ struct HistoryListItem: View {
df.string(from: entry.requestDate)
}
var roundTripText: String? {
guard let time = entry.roundTripTime else {
return nil
}
return "\(Int(time * 1000)) ms"
var roundTripText: String {
"\(Int(entry.roundTripTime * 1000)) ms"
}
var counterText: String {
@ -46,17 +43,15 @@ struct HistoryListItem: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(entry.response?.description ?? "")
Text(entry.response.description)
.font(.headline)
Spacer()
Text(entryTime)
}.padding(.bottom, 1)
HStack {
if let roundTripText {
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
Text(roundTripText)
.font(.subheadline)
}
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
Text(roundTripText)
.font(.subheadline)
Image(systemSymbol: .personalhotspot)
Text(counterText)
.font(.subheadline)

View File

@ -2,20 +2,59 @@ import SwiftUI
struct HistoryView: View {
let manager: HistoryManagerProtocol
let history: HistoryManagerProtocol
@State
private var items: [HistoryItem] = []
@State
private var unlockCount = 0
private var percentage: Double {
guard items.count > 0 else {
return 0
}
return Double(unlockCount * 100) / Double(items.count)
}
var body: some View {
NavigationView {
List(manager.loadEntries()) { entry in
HistoryListItem(entry: entry)
List {
HStack {
Text("\(items.count) requests")
.foregroundColor(.primary)
.font(.body)
Spacer()
Text(String(format: "%d successful (%.1f %%)", unlockCount, percentage))
.foregroundColor(.secondary)
.font(.footnote)
}
ForEach(items) {entry in
HistoryListItem(entry: entry)
}
}
.navigationTitle("History")
}
.onAppear {
load()
}
}
private func load() {
Task {
let entries = history.loadEntries()
DispatchQueue.main.async {
items = entries
unlockCount = items.count {
$0.response == .openSesame
}
}
}
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
HistoryView(manager: HistoryManagerMock())
HistoryView(history: HistoryManagerMock())
}
}

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -2,8 +2,7 @@ import SwiftUI
struct SettingsView: View {
@Binding
var keyManager: KeyManagement
let keyManager: KeyManagement
@Binding
var serverAddress: String
@ -88,7 +87,7 @@ struct SettingsView: View {
}.padding(.vertical, 8)
ForEach(KeyManagement.KeyType.allCases) { keyType in
SingleKeyView(
keyManager: $keyManager,
keyManager: keyManager,
type: keyType)
}
Toggle(isOn: $isCompensatingDaylightTime) {
@ -157,7 +156,7 @@ struct SettingsView: View {
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(
keyManager: .constant(KeyManagement()),
keyManager: KeyManagement(),
serverAddress: .constant("https://example.com"),
localAddress: .constant("192.168.178.42"),
deviceID: .constant(0),

View File

@ -6,8 +6,7 @@ struct SingleKeyView: View {
@State
private var needRefresh = false
@Binding
var keyManager: KeyManagement
let keyManager: KeyManagement
@State
private var showEditWindow = false
@ -74,7 +73,7 @@ struct SingleKeyView: View {
TextField("Key data", text: $keyText)
.lineLimit(4)
.font(.system(.body, design: .monospaced))
.foregroundColor(.black)
.foregroundColor(.primary)
Button("Save", action: saveKey)
Button("Cancel", role: .cancel, action: {})
}, message: {
@ -101,7 +100,7 @@ struct SingleKeyView: View {
struct SingleKeyView_Previews: PreviewProvider {
static var previews: some View {
SingleKeyView(
keyManager: .constant(KeyManagement()),
keyManager: KeyManagement(),
type: .deviceKey)
.previewLayout(.fixed(width: 350, height: 100))
}