/** * NotificationContent.swift * * Data structure for notification content * * @author Matthew Raymer * @version 1.0.0 */ import Foundation /** * Data structure representing notification content * * This class encapsulates all the information needed for a notification * including scheduling, content, and metadata. */ class NotificationContent: Codable { // MARK: - Properties let id: String let title: String? let body: String? let scheduledTime: Int64 // milliseconds since epoch (matches Android long) let fetchedAt: Int64 // milliseconds since epoch (matches Android long) let url: String? let payload: [String: Any]? let etag: String? // MARK: - Codable Support enum CodingKeys: String, CodingKey { case id case title case body case scheduledTime case fetchedAt case url case payload case etag } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) title = try container.decodeIfPresent(String.self, forKey: .title) body = try container.decodeIfPresent(String.self, forKey: .body) scheduledTime = try container.decode(Int64.self, forKey: .scheduledTime) fetchedAt = try container.decode(Int64.self, forKey: .fetchedAt) url = try container.decodeIfPresent(String.self, forKey: .url) // payload is encoded as JSON string if let payloadString = try? container.decodeIfPresent(String.self, forKey: .payload), let payloadData = payloadString.data(using: .utf8), let payloadDict = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any] { payload = payloadDict } else { payload = nil } etag = try container.decodeIfPresent(String.self, forKey: .etag) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(body, forKey: .body) try container.encode(scheduledTime, forKey: .scheduledTime) try container.encode(fetchedAt, forKey: .fetchedAt) try container.encodeIfPresent(url, forKey: .url) // Encode payload as JSON string if let payload = payload, let payloadData = try? JSONSerialization.data(withJSONObject: payload), let payloadString = String(data: payloadData, encoding: .utf8) { try container.encode(payloadString, forKey: .payload) } try container.encodeIfPresent(etag, forKey: .etag) } // MARK: - Initialization /** * Initialize notification content * * @param id Unique notification identifier * @param title Notification title * @param body Notification body text * @param scheduledTime When notification should fire (milliseconds since epoch) * @param fetchedAt When content was fetched (milliseconds since epoch) * @param url URL for content fetching * @param payload Additional payload data * @param etag ETag for HTTP caching */ init(id: String, title: String?, body: String?, scheduledTime: Int64, fetchedAt: Int64, url: String?, payload: [String: Any]?, etag: String?) { self.id = id self.title = title self.body = body self.scheduledTime = scheduledTime self.fetchedAt = fetchedAt self.url = url self.payload = payload self.etag = etag } // MARK: - Convenience Methods /** * Get scheduled time as Date * * @return Scheduled time as Date object */ func getScheduledTimeAsDate() -> Date { return Date(timeIntervalSince1970: Double(scheduledTime) / 1000.0) } /** * Get fetched time as Date * * @return Fetched time as Date object */ func getFetchedTimeAsDate() -> Date { return Date(timeIntervalSince1970: Double(fetchedAt) / 1000.0) } /** * Check if notification is scheduled for today * * @return true if scheduled for today */ func isScheduledForToday() -> Bool { let scheduledDate = getScheduledTimeAsDate() let today = Date() let calendar = Calendar.current return calendar.isDate(scheduledDate, inSameDayAs: today) } /** * Check if notification is scheduled for tomorrow * * @return true if scheduled for tomorrow */ func isScheduledForTomorrow() -> Bool { let scheduledDate = getScheduledTimeAsDate() let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() let calendar = Calendar.current return calendar.isDate(scheduledDate, inSameDayAs: tomorrow) } /** * Check if notification is in the future * * @return true if scheduled time is in the future */ func isInTheFuture() -> Bool { let currentTime = Int64(Date().timeIntervalSince1970 * 1000) return scheduledTime > currentTime } /** * Get age of content at scheduled time * * @return Age in seconds at scheduled time */ func getAgeAtScheduledTime() -> TimeInterval { return Double(scheduledTime - fetchedAt) / 1000.0 } /** * Convert to dictionary representation * * @return Dictionary representation of notification content */ func toDictionary() -> [String: Any] { return [ "id": id, "title": title ?? "", "body": body ?? "", "scheduledTime": scheduledTime, "fetchedAt": fetchedAt, "url": url ?? "", "payload": payload ?? [:], "etag": etag ?? "" ] } /** * Create from dictionary representation * * @param dict Dictionary representation * @return NotificationContent instance */ static func fromDictionary(_ dict: [String: Any]) -> NotificationContent? { guard let id = dict["id"] as? String else { return nil } // Handle both Int64 and TimeInterval (Double) for backward compatibility let scheduledTime: Int64 if let time = dict["scheduledTime"] as? Int64 { scheduledTime = time } else if let time = dict["scheduledTime"] as? Double { scheduledTime = Int64(time) } else { return nil } let fetchedAt: Int64 if let time = dict["fetchedAt"] as? Int64 { fetchedAt = time } else if let time = dict["fetchedAt"] as? Double { fetchedAt = Int64(time) } else { return nil } return NotificationContent( id: id, title: dict["title"] as? String, body: dict["body"] as? String, scheduledTime: scheduledTime, fetchedAt: fetchedAt, url: dict["url"] as? String, payload: dict["payload"] as? [String: Any], etag: dict["etag"] as? String ) } }