Files
hapi/ios/Packages/HapiKit/Sources/HapiProtocol/Models/DecryptedMessage.swift
T
weishu 5ef4304a4a fix: synchronize read state across web and native clients
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.

Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
2026-11-01 09:52:51 +08:00

101 lines
4.0 KiB
Swift

import Foundation
/// One chat message as returned by `GET /api/sessions/:id/messages` and the
/// `message-received` SSE event.
///
/// Mirrors `DecryptedMessageSchema` (`shared/src/schemas.ts`). `content` is
/// deliberately left as raw ``JSONValue``: it is the role-wrapped envelope
/// whose decode tree (`docs/api/client-contract/messages.md`) is ported in the
/// M2 chat milestones. Decoding a message is total — an absent or `null`
/// `content` degrades to `.null` instead of throwing.
public struct DecryptedMessage: Codable, Equatable, Sendable {
/// Server uuid. Optimistic rows use the client `localId` until echoed.
public var contentRevision: Int?
public var id: String
/// Per-session insert counter; `null` on optimistic rows.
public var seq: Int?
/// Client-generated id for optimistic reconciliation.
public var localId: String?
/// Role-wrapped envelope, kept wire-verbatim (decoded by the chat pipeline).
public var content: JSONValue
/// Hub receive time (epoch ms).
public var createdAt: Int
/// When the agent consumed the message (epoch ms). `nil` covers both
/// wire-`null` (still queued) and an absent key (pre-V8 hubs stamped
/// rows as already invoked without the field). Current hubs always send
/// the key, so treating `nil` as "queued" is correct against them; if
/// pre-V8 hub support is ever needed, the M2 window store must
/// distinguish explicit null (see pagination.md "Queued semantics").
public var invokedAt: Int?
/// Future-scheduled send time (epoch ms), when set.
public var scheduledAt: Int?
/// `indeterminate` means a steer outcome is unknown and needs explicit resolution.
public var deliveryState: String?
public init(
contentRevision: Int? = nil,
id: String,
seq: Int? = nil,
localId: String? = nil,
content: JSONValue = .null,
createdAt: Int,
invokedAt: Int? = nil,
scheduledAt: Int? = nil,
deliveryState: String? = nil
) {
self.contentRevision = contentRevision
self.id = id
self.seq = seq
self.localId = localId
self.content = content
self.createdAt = createdAt
self.invokedAt = invokedAt
self.scheduledAt = scheduledAt
self.deliveryState = deliveryState
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
contentRevision = try container.decodeIfPresent(Int.self, forKey: .contentRevision)
id = try container.decode(String.self, forKey: .id)
seq = try container.decodeIfPresent(Int.self, forKey: .seq)
localId = try container.decodeIfPresent(String.self, forKey: .localId)
content = try container.decodeIfPresent(JSONValue.self, forKey: .content) ?? .null
createdAt = try container.decode(Int.self, forKey: .createdAt)
invokedAt = try container.decodeIfPresent(Int.self, forKey: .invokedAt)
scheduledAt = try container.decodeIfPresent(Int.self, forKey: .scheduledAt)
deliveryState = try container.decodeIfPresent(String.self, forKey: .deliveryState)
}
}
/// Metadata describing an uploaded message attachment.
///
/// Mirrors `AttachmentMetadataSchema` (`shared/src/schemas.ts`). Used inside
/// user-message payloads and in the send-message request body.
public struct AttachmentMetadata: Codable, Equatable, Sendable {
public var id: String
public var filename: String
public var mimeType: String
/// Size in bytes.
public var size: Int
public var path: String
/// Web-serving detail; present on wire payloads, absent from fixtures.
public var previewUrl: String?
public init(
id: String,
filename: String,
mimeType: String,
size: Int,
path: String,
previewUrl: String? = nil
) {
self.id = id
self.filename = filename
self.mimeType = mimeType
self.size = size
self.path = path
self.previewUrl = previewUrl
}
}