import Foundation final class MessageStore { private let messagesKey = "kasztelan.messages" private let lastIDKey = "kasztelan.messages.lastID" private let installedAtKey = "kasztelan.installation.timestamp" var messages: [KasztelanMessage] { guard let data = UserDefaults.standard.data(forKey: messagesKey), let values = try? JSONDecoder().decode([KasztelanMessage].self, from: data) else { return [] } return values.filter(isFromThisInstallation).sorted { $0.id > $1.id } } var lastServerID: Int { UserDefaults.standard.integer(forKey: lastIDKey) } @discardableResult func merge(_ incoming: [KasztelanMessage]) -> [KasztelanMessage] { var saved = messages var ids = Set(saved.map(\.id)) let previousLast = lastServerID let added = incoming.filter { message in guard message.id > 0, isFromThisInstallation(message), !message.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return false } return ids.insert(message.id).inserted } saved.append(contentsOf: added) saved.sort { $0.id > $1.id } saved = Array(saved.prefix(100)) persist(saved, lastID: max(previousLast, incoming.map(\.id).max() ?? 0)) return added.filter { $0.id > previousLast }.sorted { $0.id > $1.id } } func delete(id: Int) { persist(messages.filter { $0.id != id }, lastID: lastServerID) } func clear() { UserDefaults.standard.removeObject(forKey: messagesKey) UserDefaults.standard.removeObject(forKey: lastIDKey) } private func persist(_ messages: [KasztelanMessage], lastID: Int) { if let data = try? JSONEncoder().encode(messages) { UserDefaults.standard.set(data, forKey: messagesKey) } UserDefaults.standard.set(lastID, forKey: lastIDKey) } private var installedAt: Int { let saved = UserDefaults.standard.integer(forKey: installedAtKey) if saved > 0 { return saved } let created = (try? FileManager.default.attributesOfItem(atPath: NSHomeDirectory())[.creationDate] as? Date) ?? Date() let timestamp = Int(created.timeIntervalSince1970) UserDefaults.standard.set(timestamp, forKey: installedAtKey) return timestamp } private func isFromThisInstallation(_ message: KasztelanMessage) -> Bool { message.sentAt >= installedAt } }