cc2c723509
Welcome: navigation section now comes first, followed by connection
info and statistics, so the main actions are visible without scrolling.
Chat bubbles: long-press opens a context menu with three actions:
- React: picks one of 6 common emojis and sends it as a message
with replyId set to the target's requestId.
- Reply: shows a reply bar above the input and attaches replyId to
the next outgoing message. Replies render a small prefix with the
quoted sender name in the bubble.
- Message sender: pushes a DM chat with the author (only for channel
chats, and never for your own messages).
Also adds a toast overlay used to surface reaction confirmations and
lightweight warnings when a message lacks a requestId.
111 lines
3.1 KiB
Swift
111 lines
3.1 KiB
Swift
import Foundation
|
|
|
|
enum DeliveryState {
|
|
case pending, confirmed, failed, none
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .pending: return "circle.dotted"
|
|
case .confirmed: return "checkmark"
|
|
case .failed: return "xmark"
|
|
case .none: return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Message: Identifiable, Hashable, Decodable {
|
|
let id: String
|
|
let text: String?
|
|
let channel: Int?
|
|
let fromNodeId: String?
|
|
let fromNodeNum: Int?
|
|
let toNodeId: String?
|
|
let timestamp: Date
|
|
let requestId: Int?
|
|
let replyId: Int?
|
|
let deliveryState: String?
|
|
let ackFailed: Int?
|
|
let portnum: Int?
|
|
let emoji: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, text, channel, fromNodeId, fromNodeNum, toNodeId
|
|
case timestamp, requestId, replyId, deliveryState, ackFailed
|
|
case portnum, emoji
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
// id may be a string or an integer; coerce to string.
|
|
if let s = try? c.decode(String.self, forKey: .id) {
|
|
id = s
|
|
} else if let n = try? c.decode(Int.self, forKey: .id) {
|
|
id = String(n)
|
|
} else {
|
|
id = UUID().uuidString
|
|
}
|
|
text = try c.decodeIfPresent(String.self, forKey: .text)
|
|
channel = try c.decodeIfPresent(Int.self, forKey: .channel)
|
|
fromNodeId = try c.decodeIfPresent(String.self, forKey: .fromNodeId)
|
|
fromNodeNum = try c.decodeIfPresent(Int.self, forKey: .fromNodeNum)
|
|
toNodeId = try c.decodeIfPresent(String.self, forKey: .toNodeId)
|
|
requestId = try c.decodeIfPresent(Int.self, forKey: .requestId)
|
|
replyId = try c.decodeIfPresent(Int.self, forKey: .replyId)
|
|
deliveryState = try c.decodeIfPresent(String.self, forKey: .deliveryState)
|
|
ackFailed = try c.decodeIfPresent(Int.self, forKey: .ackFailed)
|
|
portnum = try c.decodeIfPresent(Int.self, forKey: .portnum)
|
|
emoji = try c.decodeIfPresent(Int.self, forKey: .emoji)
|
|
|
|
if let n = try? c.decode(Double.self, forKey: .timestamp) {
|
|
let secs = n > 9_999_999_999 ? n / 1000.0 : n
|
|
timestamp = Date(timeIntervalSince1970: secs)
|
|
} else if let s = try? c.decode(String.self, forKey: .timestamp),
|
|
let parsed = Self.iso.date(from: s) {
|
|
timestamp = parsed
|
|
} else {
|
|
timestamp = Date()
|
|
}
|
|
}
|
|
|
|
private static let iso: ISO8601DateFormatter = {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return f
|
|
}()
|
|
|
|
var isText: Bool { (portnum ?? 1) == 1 }
|
|
var isReaction: Bool { emoji == 1 }
|
|
|
|
/// Integer request ID, used for replies and reactions.
|
|
/// Falls back to parsing the trailing number out of ids like "123_456".
|
|
var requestIdValue: Int? {
|
|
if let r = requestId { return r }
|
|
if let last = id.split(separator: "_").last, let n = Int(last) { return n }
|
|
return nil
|
|
}
|
|
|
|
var delivery: DeliveryState {
|
|
if ackFailed == 1 { return .failed }
|
|
switch deliveryState {
|
|
case "confirmed", "delivered": return .confirmed
|
|
case "failed", "error": return .failed
|
|
case "pending": return .pending
|
|
default: return .none
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MessageListResponse: Decodable {
|
|
let data: [Message]
|
|
}
|
|
|
|
struct SendMessageData: Decodable {
|
|
let requestId: Int?
|
|
let messageId: String?
|
|
let messageCount: Int?
|
|
}
|
|
|
|
struct SendMessageResponse: Decodable {
|
|
let data: SendMessageData?
|
|
}
|