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

@ -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.
}
}

View File

@ -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())
}
}

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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())
}
}

View File

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

View File

@ -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())

View File

@ -20,6 +20,7 @@ struct SettingsTextItemLink: View {
SettingsListTextItem(title: title, value: value)
}
.buttonStyle(PlainButtonStyle())
.padding(0)
}
}

View File

@ -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,