Delete history, allow retry
This commit is contained in:
10
Sesame/Common/ConnectionStrategy.swift
Normal file
10
Sesame/Common/ConnectionStrategy.swift
Normal 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 }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
14
Sesame/Extensions/Array+Extensions.swift
Normal file
14
Sesame/Extensions/Array+Extensions.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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))
|
||||
}
|
||||
|
Reference in New Issue
Block a user