Sesame-iOS/Sesame/ContentView.swift
2022-05-02 23:25:11 +02:00

253 lines
8.0 KiB
Swift

import SwiftUI
import CryptoKit
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
struct ContentView: View {
@AppStorage("counter")
var nextMessageCounter: Int = 0
@AppStorage("compensate")
var isCompensatingDaylightTime: Bool = 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 showKeySheet = false
@State
private var showHistorySheet = false
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("Keys", action: { showKeySheet = 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: $showKeySheet) {
KeyView(keyManager: $keyManager, isCompensatingDaylightTime: $isCompensatingDaylightTime)
}
.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, date: sentTime)
state = .waitingForResponse
print("Sending message \(count)")
Task {
let (newState, message) = await server.send(message, authToken: token)
let receivedTime = Date.now
responseTime = receivedTime
state = newState
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message)
process(item: finishedItem)
}
}
private func process(item: HistoryItem) {
guard let message = item.incomingMessage else {
save(historyItem: item)
return
}
guard let key = keyManager.get(.deviceKey) else {
save(historyItem: item.notAuthenticated())
return
}
guard message.isValid(using: key) else {
save(historyItem: item.invalidated())
return
}
nextMessageCounter = Int(message.content.id)
save(historyItem: item)
}
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 let authToken = keyManager.get(.authToken) else {
return
}
guard !hasActiveRequest else {
return
}
hasActiveRequest = true
Task {
let newState = await server.deviceStatus(authToken: authToken.data)
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))
}
}