Delete history, allow retry
This commit is contained in:
@ -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.
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
@ -20,6 +20,7 @@ struct SettingsTextItemLink: View {
|
||||
SettingsListTextItem(title: title, value: value)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user