diff --git a/Banner.key b/Banner.key index 89bde05..fd78929 100755 Binary files a/Banner.key and b/Banner.key differ diff --git a/Sesame-Watch Watch App/ComplicationController.swift b/Sesame-Watch Watch App/ComplicationController.swift deleted file mode 100644 index 30c86dc..0000000 --- a/Sesame-Watch Watch App/ComplicationController.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import ClockKit - - -class ComplicationController: NSObject, CLKComplicationDataSource { - - func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { - // TODO: Finish implementing this required method. - } -} diff --git a/Sesame-Watch Watch App/ContentView.swift b/Sesame-Watch Watch App/ContentView.swift index f3672ce..3fb9e7d 100644 --- a/Sesame-Watch Watch App/ContentView.swift +++ b/Sesame-Watch Watch App/ContentView.swift @@ -6,6 +6,9 @@ struct ContentView: View { @Binding var didLaunchFromComplication: Bool + + @AppStorage("connectionType") + var connectionType: ConnectionStrategy = .remoteFirst @AppStorage("server") var serverPath: String = "https://christophhagen.de/sesame/" @@ -19,14 +22,14 @@ struct ContentView: View { @AppStorage("compensate") var isCompensatingDaylightTime: Bool = false - @AppStorage("local") - private var useLocalConnection = false - @AppStorage("deviceId") private var deviceId: Int = 0 @EnvironmentObject var keyManager: KeyManagement + + @EnvironmentObject + var history: HistoryManager @State var state: ClientState = .noKeyAvailable @@ -36,7 +39,32 @@ struct ContentView: View { let server = Client() - let history = HistoryManager() + private var firstTryIsLocalConnection: Bool { + switch connectionType { + case .local, .localFirst: + return true + case .remote, .remoteFirst: + return false + } + } + + private var hasSecondTry: Bool { + switch connectionType { + case .localFirst, .remoteFirst: + return true + default: + return false + } + } + + private var secondTryIsLocalConnection: Bool { + switch connectionType { + case .local, .localFirst: + return false + case .remote, .remoteFirst: + return true + } + } var buttonBackground: Color { state.allowsAction ? @@ -87,45 +115,42 @@ struct ContentView: View { } func mainButtonPressed() { - guard let key = keyManager.get(.remoteKey), - let token = keyManager.get(.authToken)?.data, + guard let keys = keyManager.getAllKeys(), let deviceId = UInt8(exactly: deviceId) else { return } - let count = UInt32(nextMessageCounter) - let sentTime = Date() - // Add time to compensate that the device is using daylight savings time - let timeCompensation: UInt32 = isCompensatingDaylightTime ? 3600 : 0 - let content = Message.Content( - time: sentTime.timestamp + timeCompensation, - id: count, - device: deviceId) - let message = content.authenticate(using: key) - let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection) + sendMessage(from: deviceId, using: keys, isFirstTry: true) + } + + private func sendMessage(from deviceId: UInt8, using keys: KeySet, isFirstTry: Bool) { preventStateReset() state = .waitingForResponse - print("Sending message \(count)") + let localConnection = isFirstTry ? firstTryIsLocalConnection : secondTryIsLocalConnection Task { - let (newState, responseMessage) = await send(message, authToken: token) - let receivedTime = Date.now - state = newState - scheduleStateReset() - let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content) - guard let key = keyManager.get(.deviceKey) else { - save(historyItem: finishedItem.notAuthenticated()) + let response = await send( + count: UInt32(nextMessageCounter), + from: deviceId, + using: keys, + to: server, + over: localConnection, + while: isCompensatingDaylightTime, + localAddress: localAddress, + remoteAddress: serverPath) + + DispatchQueue.main.async { + state = response.response + scheduleStateReset() + if let counter = response.responseMessage?.id { + nextMessageCounter = Int(counter) + } + } + save(historyItem: response) + guard isFirstTry, hasSecondTry else { return } - guard let responseMessage else { - save(historyItem: finishedItem) - return + DispatchQueue.main.async { + sendMessage(from: deviceId, using: keys, isFirstTry: false) } - guard responseMessage.isValid(using: key) else { - save(historyItem: finishedItem.invalidated()) - return - } - - nextMessageCounter = Int(responseMessage.content.id) - save(historyItem: finishedItem) } } @@ -148,14 +173,6 @@ struct ContentView: View { preventStateReset() } - private func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) { - if useLocalConnection { - return await server.sendMessageOverLocalNetwork(message, server: localAddress) - } else { - return await server.send(message, server: serverPath, authToken: authToken) - } - } - private func save(historyItem: HistoryItem) { do { try history.save(item: historyItem) @@ -165,9 +182,48 @@ struct ContentView: View { } } +private func send(count: UInt32, from deviceId: UInt8, using keys: KeySet, to server: Client, over localConnection: Bool, while compensatingTime: Bool, localAddress: String, remoteAddress: String) async -> HistoryItem { + let sentTime = Date() + // Add time to compensate that the device is using daylight savings time + let timeCompensation: UInt32 = compensatingTime ? 3600 : 0 + let content = Message.Content( + time: sentTime.timestamp + timeCompensation, + id: count, + device: deviceId) + let message = content.authenticate(using: keys.remote) + print("Sending message \(count)") + let address = localConnection ? localAddress : remoteAddress + let (newState, responseMessage) = await send(message, to: server, using: keys.server, local: localConnection, address: address) + var historyItem = HistoryItem( + sent: message.content, + sentDate: sentTime, + local: localConnection, + response: newState, + responseDate: .now, + responseMessage: responseMessage?.content) + + guard let responseMessage else { + return historyItem + } + guard responseMessage.isValid(using: keys.device) else { + historyItem.response = .responseRejected(.invalidAuthentication) + return historyItem + } + return historyItem +} + +private func send(_ message: Message, to server: Client, using authToken: Data, local: Bool, address: String) async -> (state: ClientState, response: Message?) { + if local { + return await server.sendMessageOverLocalNetwork(message, server: address) + } else { + return await server.send(message, server: address, authToken: authToken) + } +} + struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(didLaunchFromComplication: .constant(false)) .environmentObject(KeyManagement()) + .environmentObject(HistoryManager()) } } diff --git a/Sesame-Watch Watch App/HistoryItemDetail.swift b/Sesame-Watch Watch App/HistoryItemDetail.swift index b72ef97..7b378e3 100644 --- a/Sesame-Watch Watch App/HistoryItemDetail.swift +++ b/Sesame-Watch Watch App/HistoryItemDetail.swift @@ -12,6 +12,10 @@ private let df: DateFormatter = { struct HistoryItemDetail: View { let item: HistoryItem + + let history: HistoryManagerProtocol + + @Environment(\.dismiss) private var dismiss private var entryTime: String { df.string(from: item.requestDate) @@ -33,7 +37,7 @@ struct HistoryItemDetail: View { List { SettingsListTextItem( title: "Status", - value: item.response?.description ?? "No response") + value: item.response.description) SettingsListTextItem( title: "Date", value: entryTime) @@ -46,22 +50,41 @@ struct HistoryItemDetail: View { SettingsListTextItem( title: "Message Counter", value: counterText) - if let time = item.roundTripTime { - SettingsListTextItem( + SettingsListTextItem( title: "Round Trip Time", - value: "\(Int(time * 1000)) ms") - } + value: "\(Int(item.roundTripTime * 1000)) ms") if let offset = item.clockOffset { SettingsListTextItem( title: "Clock offset", value: "\(offset) seconds") } + Button { + delete(item: item) + } label: { + HStack { + Spacer() + Label("Delete", systemSymbol: .trash) + Spacer() + } + } + .listRowBackground( + RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) + .fill(.red) + ) + .foregroundColor(.white) }.navigationTitle("Details") } + + private func delete(item: HistoryItem) { + guard history.delete(item: item) else { + return + } + dismiss() + } } struct HistoryItemDetail_Previews: PreviewProvider { static var previews: some View { - HistoryItemDetail(item: .mock) + HistoryItemDetail(item: .mock, history: HistoryManagerMock()) } } diff --git a/Sesame-Watch Watch App/HistoryListRow.swift b/Sesame-Watch Watch App/HistoryListRow.swift index 476b3dc..6022e7a 100644 --- a/Sesame-Watch Watch App/HistoryListRow.swift +++ b/Sesame-Watch Watch App/HistoryListRow.swift @@ -20,10 +20,11 @@ struct HistoryListRow: View { var body: some View { VStack(alignment: .leading) { HStack { - Image(systemSymbol: item.response?.symbol ?? .exclamationmarkTriangle) - Text(item.response?.description ?? "No response") + Image(systemSymbol: item.response.symbol) + Text(item.response.description) .font(.headline) .foregroundColor(.primary) + Spacer() } Text(entryTime) .font(.footnote) diff --git a/Sesame-Watch Watch App/HistoryView.swift b/Sesame-Watch Watch App/HistoryView.swift index a93a503..fe9c17d 100644 --- a/Sesame-Watch Watch App/HistoryView.swift +++ b/Sesame-Watch Watch App/HistoryView.swift @@ -2,36 +2,66 @@ import SwiftUI struct HistoryView: View { - let history: HistoryManagerProtocol + @ObservedObject + var history: HistoryManager - @State - private var items: [HistoryItem] = [] + private var unlockCount: Int { + history.entries.count { + $0.response == .openSesame + } + } + + private var percentage: Double { + guard history.entries.count > 0 else { + return 0 + } + return Double(unlockCount * 100) / Double(history.entries.count) + } var body: some View { - NavigationStack { - List(items) { item in - NavigationLink { - HistoryItemDetail(item: item) - } label: { - HistoryListRow(item: item) + NavigationView { + List { + HStack { + VStack(alignment: .leading) { + Text("\(history.entries.count) requests") + .foregroundColor(.primary) + .font(.body) + Text(String(format: "%.1f %% success", percentage)) + .foregroundColor(.secondary) + .font(.footnote) + } + Spacer() + } + .listRowBackground(Color.clear) + + ForEach(history.entries) { item in + NavigationLink { + HistoryItemDetail(item: item, history: history) + } label: { + HistoryListRow(item: item) + } + .swipeActions(edge: .trailing) { + Button { + delete(item: item) + } label: { + Label("Delete", systemSymbol: .trash) + }.tint(.red) + } } } .navigationTitle("History") - }.onAppear(perform: loadItems) + } } - private func loadItems() { - Task { - let entries = history.loadEntries() - DispatchQueue.main.async { - items = entries - } + private func delete(item: HistoryItem) { + guard history.delete(item: item) else { + return } } } struct HistoryView_Previews: PreviewProvider { static var previews: some View { - HistoryView(history: HistoryManagerMock()) + HistoryView(history: HistoryManager()) } } diff --git a/Sesame-Watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/Sesame-Watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Sesame-Watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sesame-Watch Watch App/Sesame_WatchApp.swift b/Sesame-Watch Watch App/Sesame_WatchApp.swift index 8b83d2e..da6eac8 100644 --- a/Sesame-Watch Watch App/Sesame_WatchApp.swift +++ b/Sesame-Watch Watch App/Sesame_WatchApp.swift @@ -5,6 +5,8 @@ struct Sesame_Watch_Watch_AppApp: App { let keyManagement = KeyManagement() + let history = HistoryManager() + @State var selected: Int = 0 @@ -16,11 +18,12 @@ struct Sesame_Watch_Watch_AppApp: App { TabView(selection: $selected) { ContentView(didLaunchFromComplication: $didLaunchFromComplication) .environmentObject(keyManagement) + .environmentObject(history) .tag(1) SettingsView() .environmentObject(keyManagement) .tag(2) - HistoryView(history: HistoryManager()) + HistoryView(history: history) .tag(3) } .tabViewStyle(PageTabViewStyle()) diff --git a/Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift b/Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift index 6a81eec..025afe3 100644 --- a/Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift +++ b/Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift @@ -20,6 +20,7 @@ struct SettingsTextItemLink: View { SettingsListTextItem(title: title, value: value) } .buttonStyle(PlainButtonStyle()) + .padding(0) } } diff --git a/Sesame-Watch Watch App/SettingsView.swift b/Sesame-Watch Watch App/SettingsView.swift index 1af38e4..d3647cc 100644 --- a/Sesame-Watch Watch App/SettingsView.swift +++ b/Sesame-Watch Watch App/SettingsView.swift @@ -2,6 +2,9 @@ import SwiftUI struct SettingsView: View { + @AppStorage("connectionType") + var connectionType: ConnectionStrategy = .remoteFirst + @AppStorage("server") var serverPath: String = "https://christophhagen.de/sesame/" @@ -14,9 +17,6 @@ struct SettingsView: View { @AppStorage("compensate") var isCompensatingDaylightTime: Bool = false - @AppStorage("local") - private var useLocalConnection = false - @AppStorage("deviceId") private var deviceId: Int = 0 @@ -26,6 +26,17 @@ struct SettingsView: View { var body: some View { NavigationStack { List { + Picker("Connection", selection: $connectionType) { + Text(ConnectionStrategy.local.rawValue) + .tag(ConnectionStrategy.local) + Text(ConnectionStrategy.localFirst.rawValue) + .tag(ConnectionStrategy.localFirst) + Text(ConnectionStrategy.remote.rawValue) + .tag(ConnectionStrategy.remote) + Text(ConnectionStrategy.remoteFirst.rawValue) + .tag(ConnectionStrategy.remoteFirst) + } + .padding(.leading) SettingsTextItemLink( title: "Server url", value: $serverPath, @@ -34,10 +45,6 @@ struct SettingsView: View { title: "Local url", value: $localAddress, footnote: "The url where the device can be reached directly on the local WiFi network.") - SettingsListToggleItem( - title: "Local connection", - value: $useLocalConnection, - subtitle: "Attempt to communicate directly with the device, which requires a WiFi connection on the same network.") SettingsNumberItemLink( title: "Device ID", value: $deviceId, diff --git a/Sesame.xcodeproj/project.pbxproj b/Sesame.xcodeproj/project.pbxproj index 88f95bd..2306fab 100644 --- a/Sesame.xcodeproj/project.pbxproj +++ b/Sesame.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B6279F48C100D6E650 /* SesameApp.swift */; }; 884A45B9279F48C100D6E650 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* ContentView.swift */; }; 884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; }; - 884A45BE279F48C300D6E650 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */; }; 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; }; 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; }; 884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; }; @@ -22,7 +21,6 @@ 88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */; }; 88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; }; 88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; }; - 88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; }; 88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; 88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; 88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; }; @@ -37,7 +35,6 @@ 88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */; }; 88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */; }; 88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; }; - 88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; }; E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */; }; E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */; }; E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */; }; @@ -54,7 +51,9 @@ E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; }; E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; }; E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; }; - E268E04D2A852AFE00185913 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268E04C2A852AFE00185913 /* ComplicationController.swift */; }; + E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; }; + E24F6C6F2A8974C60040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; }; + E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; }; E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0532A852F8E00185913 /* WidgetKit.framework */; }; E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0552A852F8E00185913 /* SwiftUI.framework */; }; E268E0862A85302000185913 /* Sesame_Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268E0852A85302000185913 /* Sesame_Widget.swift */; }; @@ -70,6 +69,8 @@ E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; }; E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */; }; E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */; }; + E2F5DCCA2A88E913002858B9 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */; }; + E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -101,7 +102,6 @@ 884A45B6279F48C100D6E650 /* SesameApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameApp.swift; sourceTree = ""; }; 884A45B8279F48C100D6E650 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = ""; }; 884A45C827A43D7900D6E650 /* ClientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientState.swift; sourceTree = ""; }; 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = ""; }; @@ -113,7 +113,6 @@ 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = ""; }; 88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = ""; }; E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListToggleItem.swift; sourceTree = ""; }; @@ -128,7 +127,7 @@ E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = ""; }; E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; - E268E04C2A852AFE00185913 /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; + E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = ""; }; E268E0532A852F8E00185913 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; E268E0552A852F8E00185913 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; E268E0812A85302000185913 /* Sesame-WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Sesame-WidgetExtension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -145,6 +144,7 @@ E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteAPI.swift; sourceTree = ""; }; E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extensions.swift"; sourceTree = ""; }; E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessage.swift; sourceTree = ""; }; + E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -206,36 +206,24 @@ isa = PBXGroup; children = ( E28DED38281EE9CF00259690 /* Info.plist */, + 884A45BA279F48C300D6E650 /* Assets.xcassets */, + E24F6C6C2A89748B0040F8C4 /* Common */, E2C5C1D92806FE4A00769EF6 /* API */, 884A45B6279F48C100D6E650 /* SesameApp.swift */, 884A45B8279F48C100D6E650 /* ContentView.swift */, - E28DED30281EAE9100259690 /* HistoryView.swift */, - E28DED32281EB15B00259690 /* HistoryListItem.swift */, - E28DED34281EB17600259690 /* HistoryItem.swift */, - E28DED36281EC7FB00259690 /* HistoryManager.swift */, E28DED2C281E840B00259690 /* SettingsView.swift */, E28DED2E281E8A0500259690 /* SingleKeyView.swift */, - 884A45C827A43D7900D6E650 /* ClientState.swift */, - 884A45CC27A465F500D6E650 /* Client.swift */, - 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */, - 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */, - 884A45BA279F48C300D6E650 /* Assets.xcassets */, - 884A45BC279F48C300D6E650 /* Preview Content */, + E28DED30281EAE9100259690 /* HistoryView.swift */, + E25317542A8A1A07005A537D /* History */, + E25317552A8A1A32005A537D /* Extensions */, ); path = Sesame; sourceTree = ""; }; - 884A45BC279F48C300D6E650 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = { isa = PBXGroup; children = ( + 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */, E24065562A819AAD009C1AD8 /* Settings */, 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */, 88E197B329EDC9BC00BF1D19 /* ContentView.swift */, @@ -243,22 +231,11 @@ 888362352A80F4420032BBB2 /* HistoryView.swift */, E240655D2A822E97009C1AD8 /* HistoryListRow.swift */, E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */, - 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */, - 88E197B729EDC9BD00BF1D19 /* Preview Content */, 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */, - E268E04C2A852AFE00185913 /* ComplicationController.swift */, ); path = "Sesame-Watch Watch App"; sourceTree = ""; }; - 88E197B729EDC9BD00BF1D19 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 88E197CA29EDCD4900BF1D19 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -283,12 +260,42 @@ path = Settings; sourceTree = ""; }; + E24F6C6C2A89748B0040F8C4 /* Common */ = { + isa = PBXGroup; + children = ( + 884A45CC27A465F500D6E650 /* Client.swift */, + 884A45C827A43D7900D6E650 /* ClientState.swift */, + E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */, + E28DED36281EC7FB00259690 /* HistoryManager.swift */, + 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */, + ); + path = Common; + sourceTree = ""; + }; + E25317542A8A1A07005A537D /* History */ = { + isa = PBXGroup; + children = ( + E28DED32281EB15B00259690 /* HistoryListItem.swift */, + E28DED34281EB17600259690 /* HistoryItem.swift */, + ); + path = History; + sourceTree = ""; + }; + E25317552A8A1A32005A537D /* Extensions */ = { + isa = PBXGroup; + children = ( + E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */, + 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; E268E0842A85302000185913 /* Sesame-Widget */ = { isa = PBXGroup; children = ( - E268E0852A85302000185913 /* Sesame_Widget.swift */, - E268E0872A85302000185913 /* Assets.xcassets */, E268E0892A85302000185913 /* Info.plist */, + E268E0872A85302000185913 /* Assets.xcassets */, + E268E0852A85302000185913 /* Sesame_Widget.swift */, ); path = "Sesame-Widget"; sourceTree = ""; @@ -428,7 +435,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 884A45BE279F48C300D6E650 /* Preview Assets.xcassets in Resources */, 884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -437,7 +443,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */, 88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -468,12 +473,14 @@ 884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */, E28DED31281EAE9100259690 /* HistoryView.swift in Sources */, E24EE77927FF95E00011CFD2 /* Message.swift in Sources */, + E2F5DCCA2A88E913002858B9 /* Array+Extensions.swift in Sources */, E28DED35281EB17600259690 /* HistoryItem.swift in Sources */, 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */, E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */, E28DED2D281E840B00259690 /* SettingsView.swift in Sources */, 884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */, 88E197C429EDCC8900BF1D19 /* Client.swift in Sources */, + E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */, 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */, E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */, ); @@ -485,6 +492,7 @@ files = ( 888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */, 88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */, + E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */, E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */, 888362362A80F4420032BBB2 /* HistoryView.swift in Sources */, E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */, @@ -500,7 +508,7 @@ 88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */, E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */, 88E197C929EDCCE100BF1D19 /* Message.swift in Sources */, - 88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */, + E24F6C6F2A8974C60040F8C4 /* ConnectionStrategy.swift in Sources */, E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */, 88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */, 88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */, @@ -508,7 +516,7 @@ E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */, E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */, E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */, - E268E04D2A852AFE00185913 /* ComplicationController.swift in Sources */, + E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */, 88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */, E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */, ); @@ -657,7 +665,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -668,6 +676,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -691,7 +700,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -702,6 +711,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -725,7 +735,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sesame-Watch Watch App/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -757,7 +767,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sesame-Watch Watch App/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = H8WR4M6QQ4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate b/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate index c450514..6f7fa7f 100644 Binary files a/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate and b/Sesame.xcodeproj/project.xcworkspace/xcuserdata/ch.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sesame/Client.swift b/Sesame/Common/Client.swift similarity index 100% rename from Sesame/Client.swift rename to Sesame/Common/Client.swift diff --git a/Sesame/ClientState.swift b/Sesame/Common/ClientState.swift similarity index 100% rename from Sesame/ClientState.swift rename to Sesame/Common/ClientState.swift diff --git a/Sesame/Common/ConnectionStrategy.swift b/Sesame/Common/ConnectionStrategy.swift new file mode 100644 index 0000000..37ff2b1 --- /dev/null +++ b/Sesame/Common/ConnectionStrategy.swift @@ -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 } +} diff --git a/Sesame/HistoryManager.swift b/Sesame/Common/HistoryManager.swift similarity index 56% rename from Sesame/HistoryManager.swift rename to Sesame/Common/HistoryManager.swift index fb9b5a6..134594c 100644 --- a/Sesame/HistoryManager.swift +++ b/Sesame/Common/HistoryManager.swift @@ -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 } } diff --git a/Sesame/KeyManagement.swift b/Sesame/Common/KeyManagement.swift similarity index 90% rename from Sesame/KeyManagement.swift rename to Sesame/Common/KeyManagement.swift index 9cbbe80..e235526 100644 --- a/Sesame/KeyManagement.swift +++ b/Sesame/Common/KeyManagement.swift @@ -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) } diff --git a/Sesame/ContentView.swift b/Sesame/ContentView.swift index d517cfd..82495d7 100644 --- a/Sesame/ContentView.swift +++ b/Sesame/ContentView.swift @@ -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) diff --git a/Sesame/Extensions/Array+Extensions.swift b/Sesame/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..1ffedad --- /dev/null +++ b/Sesame/Extensions/Array+Extensions.swift @@ -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 + } +} diff --git a/Sesame/SymmetricKey+Extensions.swift b/Sesame/Extensions/SymmetricKey+Extensions.swift similarity index 100% rename from Sesame/SymmetricKey+Extensions.swift rename to Sesame/Extensions/SymmetricKey+Extensions.swift diff --git a/Sesame/HistoryItem.swift b/Sesame/History/HistoryItem.swift similarity index 57% rename from Sesame/HistoryItem.swift rename to Sesame/History/HistoryItem.swift index a975b0b..e1e9702 100644 --- a/Sesame/HistoryItem.swift +++ b/Sesame/History/HistoryItem.swift @@ -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) } } diff --git a/Sesame/HistoryListItem.swift b/Sesame/History/HistoryListItem.swift similarity index 78% rename from Sesame/HistoryListItem.swift rename to Sesame/History/HistoryListItem.swift index 3521cdc..5c65e7e 100644 --- a/Sesame/HistoryListItem.swift +++ b/Sesame/History/HistoryListItem.swift @@ -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) diff --git a/Sesame/HistoryView.swift b/Sesame/HistoryView.swift index 5785bb3..e854366 100644 --- a/Sesame/HistoryView.swift +++ b/Sesame/HistoryView.swift @@ -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()) } } diff --git a/Sesame/Preview Content/Preview Assets.xcassets/Contents.json b/Sesame/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Sesame/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sesame/SettingsView.swift b/Sesame/SettingsView.swift index e216dfc..a69500c 100644 --- a/Sesame/SettingsView.swift +++ b/Sesame/SettingsView.swift @@ -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), diff --git a/Sesame/SingleKeyView.swift b/Sesame/SingleKeyView.swift index 667c928..98eade3 100644 --- a/Sesame/SingleKeyView.swift +++ b/Sesame/SingleKeyView.swift @@ -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)) }