Sesame-iOS/Sesame/ContentView.swift
2023-04-11 18:18:31 +02:00

282 lines
9.1 KiB
Swift

import SwiftUI
import CryptoKit
struct ContentView: View {
@AppStorage("server")
var serverPath: String = "https://christophhagen.de/sesame/"
@AppStorage("localIP")
var localAddress: String = "192.168.178.104/"
@AppStorage("counter")
var nextMessageCounter: Int = 0
@AppStorage("compensate")
var isCompensatingDaylightTime: Bool = false
@AppStorage("local")
private var useLocalConnection = false
@State
var keyManager = KeyManagement()
let history = HistoryManager()
@State
var state: ClientState = .noKeyAvailable
@State
private var timer: Timer?
@State
private var hasActiveRequest = false
@State
private var responseTime: Date? = nil
@State
private var showSettingsSheet = false
@State
private var showHistorySheet = false
@State
private var didShowKeySheetOnce = false
let server = Client()
var compensationTime: UInt32 {
isCompensatingDaylightTime ? 3600 : 0
}
var isPerformingRequests: Bool {
hasActiveRequest ||
state == .waitingForResponse
}
var buttonBackground: Color {
state.allowsAction ?
.white.opacity(0.2) :
.black.opacity(0.2)
}
let buttonBorderWidth: CGFloat = 3
var buttonColor: Color {
state.allowsAction ? .white : .gray
}
private let buttonWidth: CGFloat = 250
private let smallButtonHeight: CGFloat = 50
private let smallButtonWidth: CGFloat = 120
private let smallButtonBorderWidth: CGFloat = 1
var body: some View {
GeometryReader { geo in
VStack(spacing: 20) {
HStack {
Button("History", action: { showHistorySheet = true })
.frame(width: smallButtonWidth,
height: smallButtonHeight)
.background(.white.opacity(0.2))
.cornerRadius(smallButtonHeight / 2)
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
.foregroundColor(.white)
.font(.title2)
.padding()
Spacer()
Button("Settings", action: { showSettingsSheet = true })
.frame(width: smallButtonWidth,
height: smallButtonHeight)
.background(.white.opacity(0.2))
.cornerRadius(smallButtonHeight / 2)
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
.foregroundColor(.white)
.font(.title2)
.padding()
}
Spacer()
if state.requiresDescription {
Text(state.description)
.padding()
}
Button(state.actionText, action: mainButtonPressed)
.frame(width: buttonWidth,
height: buttonWidth)
.background(buttonBackground)
.cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
.foregroundColor(buttonColor)
.font(.title)
.disabled(!state.allowsAction)
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
}
.background(state.color)
.onAppear {
if keyManager.hasAllKeys {
state = .requestingStatus
}
startRegularStatusUpdates()
}
.onDisappear {
endRegularStatusUpdates()
}
.frame(width: geo.size.width, height: geo.size.height)
.animation(.easeInOut, value: state.color)
.sheet(isPresented: $showSettingsSheet) {
SettingsView(
keyManager: $keyManager,
serverAddress: $serverPath,
localAddress: $localAddress,
isCompensatingDaylightTime: $isCompensatingDaylightTime,
useLocalConnection: $useLocalConnection)
}
.sheet(isPresented: $showHistorySheet) {
HistoryView(manager: history)
}
}
.preferredColorScheme(.dark)
}
func mainButtonPressed() {
guard let key = keyManager.get(.remoteKey),
let token = keyManager.get(.authToken)?.data else {
return
}
let count = UInt32(nextMessageCounter)
let sentTime = Date()
// Add time to compensate that the device is using daylight savings time
let content = Message.Content(
time: sentTime.timestamp + compensationTime,
id: count)
let message = content.authenticate(using: key)
let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
state = .waitingForResponse
print("Sending message \(count)")
Task {
let (newState, responseMessage) = await send(message, authToken: token)
let receivedTime = Date.now
responseTime = receivedTime
state = newState
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content)
guard let key = keyManager.get(.deviceKey) else {
save(historyItem: finishedItem.notAuthenticated())
return
}
guard let responseMessage else {
save(historyItem: finishedItem)
return
}
guard responseMessage.isValid(using: key) else {
save(historyItem: finishedItem.invalidated())
return
}
nextMessageCounter = Int(responseMessage.content.id)
save(historyItem: finishedItem)
}
}
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)
} catch {
print("Failed to save item: \(error)")
}
}
private func startRegularStatusUpdates() {
guard timer == nil else {
return
}
DispatchQueue.main.async {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
timer!.fire()
}
}
private func endRegularStatusUpdates() {
timer?.invalidate()
timer = nil
}
func checkDeviceStatus(_ timer: Timer) {
guard !useLocalConnection else {
return
}
guard let authToken = keyManager.get(.authToken) else {
if !didShowKeySheetOnce {
didShowKeySheetOnce = true
//showSettingsSheet = true
}
return
}
guard !hasActiveRequest else {
return
}
hasActiveRequest = true
Task {
let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath)
hasActiveRequest = false
switch state {
case .noKeyAvailable:
return
case .requestingStatus, .deviceNotAvailable, .ready:
state = newState
case .waitingForResponse:
return
case .messageRejected, .openSesame, .internalError, .responseRejected:
guard let time = responseTime else {
state = newState
return
}
responseTime = nil
// Wait at least 5 seconds after these states have been reached before changing the
// interface to allow sufficient time to see the result
let elapsed = Date.now.timeIntervalSince(time)
guard elapsed < 5 else {
state = newState
return
}
let secondsToWait = Int(elapsed.rounded(.up))
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) {
state = newState
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 8")
}
}
extension Date {
var timestamp: UInt32 {
UInt32(timeIntervalSince1970.rounded())
}
init(timestamp: UInt32) {
self.init(timeIntervalSince1970: TimeInterval(timestamp))
}
}