@zellio/chat-react-native 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +239 -2
- package/ZellioChatReactNative.podspec +30 -0
- package/android/build.gradle +62 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatModule.kt +214 -0
- package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatPackage.kt +25 -0
- package/app.plugin.js +4 -0
- package/ios/RNZellioChat.h +5 -0
- package/ios/RNZellioChat.mm +116 -0
- package/ios/ZCRNBridge.swift +217 -0
- package/ios/vendor/resources/PrivacyInfo.xcprivacy +99 -0
- package/ios/vendor/swift/ZellioChat/Model/ChatModel.swift +261 -0
- package/ios/vendor/swift/ZellioChat/Platform/Platform.swift +124 -0
- package/ios/vendor/swift/ZellioChat/Platform/URLSessionTransport.swift +175 -0
- package/ios/vendor/swift/ZellioChat/Preview/Previews.swift +62 -0
- package/ios/vendor/swift/ZellioChat/Public/InAppBanner.swift +155 -0
- package/ios/vendor/swift/ZellioChat/Public/PublicTypes.swift +141 -0
- package/ios/vendor/swift/ZellioChat/Public/Runtime.swift +406 -0
- package/ios/vendor/swift/ZellioChat/Public/ZellioChat.swift +209 -0
- package/ios/vendor/swift/ZellioChat/Theme/ChatTheme.swift +185 -0
- package/ios/vendor/swift/ZellioChat/Thread/AttachmentPicker.swift +354 -0
- package/ios/vendor/swift/ZellioChat/Thread/ComposerBar.swift +355 -0
- package/ios/vendor/swift/ZellioChat/Thread/ThreadCells.swift +776 -0
- package/ios/vendor/swift/ZellioChat/Thread/ThreadViewController.swift +703 -0
- package/ios/vendor/swift/ZellioChat/Views/ChatRootView.swift +154 -0
- package/ios/vendor/swift/ZellioChat/Views/Components.swift +335 -0
- package/ios/vendor/swift/ZellioChat/Views/HomeView.swift +413 -0
- package/ios/vendor/swift/ZellioChatCore/ChatModels.swift +373 -0
- package/ios/vendor/swift/ZellioChatCore/ConfigModels.swift +389 -0
- package/ios/vendor/swift/ZellioChatCore/Engine.swift +325 -0
- package/ios/vendor/swift/ZellioChatCore/Errors.swift +158 -0
- package/ios/vendor/swift/ZellioChatCore/Generated/ChatStringTable.swift +125 -0
- package/ios/vendor/swift/ZellioChatCore/Outbox.swift +223 -0
- package/ios/vendor/swift/ZellioChatCore/Push.swift +74 -0
- package/ios/vendor/swift/ZellioChatCore/Realtime.swift +328 -0
- package/ios/vendor/swift/ZellioChatCore/Session.swift +210 -0
- package/ios/vendor/swift/ZellioChatCore/Storage.swift +201 -0
- package/ios/vendor/swift/ZellioChatCore/Strings.swift +104 -0
- package/ios/vendor/swift/ZellioChatCore/Theme.swift +238 -0
- package/ios/vendor/swift/ZellioChatCore/ThreadController.swift +319 -0
- package/ios/vendor/swift/ZellioChatCore/Timeline.swift +157 -0
- package/ios/vendor/swift/ZellioChatCore/Transport.swift +236 -0
- package/ios/vendor/swift/ZellioChatCore/Util.swift +137 -0
- package/lib/NativeZellioChat.d.ts +34 -0
- package/lib/NativeZellioChat.js +4 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +241 -0
- package/lib/types.d.ts +54 -0
- package/lib/types.js +2 -0
- package/lib/version.d.ts +4 -0
- package/lib/version.js +7 -0
- package/package.json +75 -4
- package/plugin/build/index.js +73 -0
- package/src/NativeZellioChat.ts +30 -0
- package/src/index.ts +237 -0
- package/src/types.ts +56 -0
- package/src/version.ts +5 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public enum MessageSender: String, Codable, Equatable, Sendable {
|
|
4
|
+
case visitor
|
|
5
|
+
case agent
|
|
6
|
+
case unknown
|
|
7
|
+
|
|
8
|
+
public init(from decoder: Decoder) throws {
|
|
9
|
+
let value = (try? decoder.singleValueContainer().decode(String.self)) ?? ""
|
|
10
|
+
self = MessageSender(rawValue: value) ?? .unknown
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public enum DeliveryState: String, Codable, Equatable, Sendable {
|
|
15
|
+
case sent
|
|
16
|
+
case sending
|
|
17
|
+
case waiting // queued, offline
|
|
18
|
+
case failed
|
|
19
|
+
|
|
20
|
+
public init(from decoder: Decoder) throws {
|
|
21
|
+
let value = (try? decoder.singleValueContainer().decode(String.self)) ?? ""
|
|
22
|
+
self = DeliveryState(rawValue: value) ?? .sent
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public struct Attachment: Codable, Equatable, Sendable {
|
|
27
|
+
public var id: String?
|
|
28
|
+
public var kind: String // "image" or "file"
|
|
29
|
+
public var url: String
|
|
30
|
+
public var name: String
|
|
31
|
+
public var mimeType: String
|
|
32
|
+
public var size: Int?
|
|
33
|
+
public var width: Int?
|
|
34
|
+
public var height: Int?
|
|
35
|
+
|
|
36
|
+
public init(id: String? = nil, kind: String, url: String, name: String, mimeType: String, size: Int? = nil, width: Int? = nil, height: Int? = nil) {
|
|
37
|
+
self.id = id
|
|
38
|
+
self.kind = kind
|
|
39
|
+
self.url = url
|
|
40
|
+
self.name = name
|
|
41
|
+
self.mimeType = mimeType
|
|
42
|
+
self.size = size
|
|
43
|
+
self.width = width
|
|
44
|
+
self.height = height
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public var isImage: Bool { kind == "image" || mimeType.hasPrefix("image/") }
|
|
48
|
+
|
|
49
|
+
private enum CodingKeys: String, CodingKey { case id, kind, url, name, mimeType, size, width, height }
|
|
50
|
+
|
|
51
|
+
public init(from decoder: Decoder) throws {
|
|
52
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
53
|
+
id = c.lenient(String.self, .id)
|
|
54
|
+
let mime = c.lenient(String.self, .mimeType) ?? ""
|
|
55
|
+
mimeType = mime
|
|
56
|
+
kind = c.lenient(String.self, .kind) ?? (mime.hasPrefix("image/") ? "image" : "file")
|
|
57
|
+
url = c.lenient(String.self, .url) ?? ""
|
|
58
|
+
name = c.lenient(String.self, .name) ?? ""
|
|
59
|
+
size = c.lenient(Int.self, .size)
|
|
60
|
+
width = c.lenient(Int.self, .width)
|
|
61
|
+
height = c.lenient(Int.self, .height)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// The page the customer is on. Sent with messages. Strings are at most 200 characters, `url` must be https.
|
|
66
|
+
public struct ChatContext: Codable, Equatable, Sendable {
|
|
67
|
+
public var screen: String?
|
|
68
|
+
public var itemType: String?
|
|
69
|
+
public var itemId: String?
|
|
70
|
+
public var itemTitle: String?
|
|
71
|
+
public var url: String?
|
|
72
|
+
|
|
73
|
+
public init(screen: String? = nil, itemType: String? = nil, itemId: String? = nil, itemTitle: String? = nil, url: String? = nil) {
|
|
74
|
+
self.screen = screen
|
|
75
|
+
self.itemType = itemType
|
|
76
|
+
self.itemId = itemId
|
|
77
|
+
self.itemTitle = itemTitle
|
|
78
|
+
self.url = url
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public var isEmpty: Bool { screen == nil && itemType == nil && itemId == nil && itemTitle == nil && url == nil }
|
|
82
|
+
|
|
83
|
+
/// Trims every string to 200 UTF-16 units and drops a url that does not start with lowercase https://. Nil when nothing is left.
|
|
84
|
+
public func sanitized() -> ChatContext? {
|
|
85
|
+
func clip(_ value: String?) -> String? {
|
|
86
|
+
guard let value = value, !value.isEmpty else { return nil }
|
|
87
|
+
return value.clipped(toUTF16: 200)
|
|
88
|
+
}
|
|
89
|
+
var safeURL = clip(url)
|
|
90
|
+
if let candidate = safeURL, !candidate.hasPrefix("https://") { safeURL = nil }
|
|
91
|
+
let result = ChatContext(screen: clip(screen), itemType: clip(itemType), itemId: clip(itemId), itemTitle: clip(itemTitle), url: safeURL)
|
|
92
|
+
return result.isEmpty ? nil : result
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public struct ChatMessage: Codable, Equatable, Sendable, Identifiable {
|
|
97
|
+
public var serverId: String?
|
|
98
|
+
public var clientMessageId: String?
|
|
99
|
+
public var from: MessageSender
|
|
100
|
+
public var text: String
|
|
101
|
+
public var attachments: [Attachment]
|
|
102
|
+
public var createdAt: String
|
|
103
|
+
public var agentName: String?
|
|
104
|
+
public var agentAvatarUrl: String?
|
|
105
|
+
public var delivery: DeliveryState
|
|
106
|
+
public var failureReason: String?
|
|
107
|
+
|
|
108
|
+
public init(serverId: String? = nil, clientMessageId: String? = nil, from: MessageSender, text: String, attachments: [Attachment] = [],
|
|
109
|
+
createdAt: String, agentName: String? = nil, agentAvatarUrl: String? = nil, delivery: DeliveryState = .sent, failureReason: String? = nil) {
|
|
110
|
+
self.serverId = serverId
|
|
111
|
+
self.clientMessageId = clientMessageId
|
|
112
|
+
self.from = from
|
|
113
|
+
self.text = text
|
|
114
|
+
self.attachments = attachments
|
|
115
|
+
self.createdAt = createdAt
|
|
116
|
+
self.agentName = agentName
|
|
117
|
+
self.agentAvatarUrl = agentAvatarUrl
|
|
118
|
+
self.delivery = delivery
|
|
119
|
+
self.failureReason = failureReason
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private enum CodingKeys: String, CodingKey {
|
|
123
|
+
case serverId = "id"
|
|
124
|
+
case clientMessageId, from, text, attachments, createdAt, agentName, agentAvatarUrl, delivery, failureReason
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public init(from decoder: Decoder) throws {
|
|
128
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
129
|
+
serverId = c.lenient(String.self, .serverId)
|
|
130
|
+
clientMessageId = c.lenient(String.self, .clientMessageId)
|
|
131
|
+
from = c.lenient(MessageSender.self, .from) ?? .unknown
|
|
132
|
+
text = c.lenient(String.self, .text) ?? ""
|
|
133
|
+
attachments = c.lenient(LossyArray<Attachment>.self, .attachments)?.items ?? []
|
|
134
|
+
createdAt = c.lenient(String.self, .createdAt) ?? ""
|
|
135
|
+
agentName = c.lenient(String.self, .agentName)
|
|
136
|
+
agentAvatarUrl = c.lenient(String.self, .agentAvatarUrl)
|
|
137
|
+
delivery = c.lenient(DeliveryState.self, .delivery) ?? .sent
|
|
138
|
+
failureReason = c.lenient(String.self, .failureReason)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Stable for the whole life of a message: the same value before and after the server confirms it.
|
|
142
|
+
public var stableId: String { clientMessageId ?? serverId ?? createdAt }
|
|
143
|
+
public var id: String { stableId }
|
|
144
|
+
public var date: Date { ISODate.parse(createdAt) ?? Date(timeIntervalSince1970: 0) }
|
|
145
|
+
public var isUsable: Bool { from != .unknown && (serverId != nil || clientMessageId != nil) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public struct MessagesPage: Equatable, Sendable {
|
|
149
|
+
public var conversationId: String?
|
|
150
|
+
public var messages: [ChatMessage]
|
|
151
|
+
public var nextBefore: String?
|
|
152
|
+
public var latest: String?
|
|
153
|
+
|
|
154
|
+
public init(conversationId: String? = nil, messages: [ChatMessage] = [], nextBefore: String? = nil, latest: String? = nil) {
|
|
155
|
+
self.conversationId = conversationId
|
|
156
|
+
self.messages = messages
|
|
157
|
+
self.nextBefore = nextBefore
|
|
158
|
+
self.latest = latest
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
extension MessagesPage: Decodable {
|
|
163
|
+
private enum CodingKeys: String, CodingKey { case conversationId, messages, nextBefore, latest }
|
|
164
|
+
|
|
165
|
+
public init(from decoder: Decoder) throws {
|
|
166
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
167
|
+
conversationId = c.lenient(String.self, .conversationId)
|
|
168
|
+
// Unknown `from` and items without an id are skipped.
|
|
169
|
+
messages = (c.lenient(LossyArray<ChatMessage>.self, .messages)?.items ?? []).filter { $0.isUsable }
|
|
170
|
+
nextBefore = c.lenient(String.self, .nextBefore)
|
|
171
|
+
latest = c.lenient(String.self, .latest)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public struct LastMessage: Codable, Equatable, Sendable {
|
|
176
|
+
public var from: MessageSender
|
|
177
|
+
public var text: String
|
|
178
|
+
public var hasAttachment: Bool
|
|
179
|
+
/// "image", "file" or nil. A value the SDK does not know is treated as a file.
|
|
180
|
+
public var attachmentKind: String?
|
|
181
|
+
|
|
182
|
+
public init(from: MessageSender, text: String, hasAttachment: Bool, attachmentKind: String? = nil) {
|
|
183
|
+
self.from = from
|
|
184
|
+
self.text = text
|
|
185
|
+
self.hasAttachment = hasAttachment
|
|
186
|
+
self.attachmentKind = attachmentKind
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private enum CodingKeys: String, CodingKey { case from, text, hasAttachment, attachmentKind }
|
|
190
|
+
|
|
191
|
+
public init(from decoder: Decoder) throws {
|
|
192
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
193
|
+
from = c.lenient(MessageSender.self, .from) ?? .unknown
|
|
194
|
+
text = c.lenient(String.self, .text) ?? ""
|
|
195
|
+
hasAttachment = c.lenient(Bool.self, .hasAttachment) ?? false
|
|
196
|
+
attachmentKind = c.lenient(String.self, .attachmentKind)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public struct ConversationSummary: Codable, Equatable, Sendable, Identifiable {
|
|
201
|
+
public var id: String
|
|
202
|
+
public var status: String
|
|
203
|
+
public var lastMessageAt: String?
|
|
204
|
+
public var lastMessage: LastMessage?
|
|
205
|
+
public var agentName: String?
|
|
206
|
+
public var agentAvatarUrl: String?
|
|
207
|
+
public var unread: Int
|
|
208
|
+
|
|
209
|
+
public init(id: String, status: String = "open", lastMessageAt: String? = nil, lastMessage: LastMessage? = nil, agentName: String? = nil, agentAvatarUrl: String? = nil, unread: Int = 0) {
|
|
210
|
+
self.id = id
|
|
211
|
+
self.status = status
|
|
212
|
+
self.lastMessageAt = lastMessageAt
|
|
213
|
+
self.lastMessage = lastMessage
|
|
214
|
+
self.agentName = agentName
|
|
215
|
+
self.agentAvatarUrl = agentAvatarUrl
|
|
216
|
+
self.unread = unread
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private enum CodingKeys: String, CodingKey { case id, status, lastMessageAt, lastMessage, agentName, agentAvatarUrl, unread }
|
|
220
|
+
|
|
221
|
+
public init(from decoder: Decoder) throws {
|
|
222
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
223
|
+
guard let id = c.lenient(String.self, .id), !id.isEmpty else {
|
|
224
|
+
throw DecodingError.keyNotFound(CodingKeys.id, DecodingError.Context(codingPath: [], debugDescription: "id"))
|
|
225
|
+
}
|
|
226
|
+
self.id = id
|
|
227
|
+
status = c.lenient(String.self, .status) ?? "open"
|
|
228
|
+
lastMessageAt = c.lenient(String.self, .lastMessageAt)
|
|
229
|
+
lastMessage = c.lenient(LastMessage.self, .lastMessage)
|
|
230
|
+
agentName = c.lenient(String.self, .agentName)
|
|
231
|
+
agentAvatarUrl = c.lenient(String.self, .agentAvatarUrl)
|
|
232
|
+
unread = c.lenient(Int.self, .unread) ?? 0
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public var lastDate: Date? { lastMessageAt.flatMap { ISODate.parse($0) } }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public struct ConversationsPage: Codable, Equatable, Sendable {
|
|
239
|
+
public var conversations: [ConversationSummary]
|
|
240
|
+
public var nextCursor: String?
|
|
241
|
+
public var unreadTotal: Int
|
|
242
|
+
|
|
243
|
+
public init(conversations: [ConversationSummary] = [], nextCursor: String? = nil, unreadTotal: Int = 0) {
|
|
244
|
+
self.conversations = conversations
|
|
245
|
+
self.nextCursor = nextCursor
|
|
246
|
+
self.unreadTotal = unreadTotal
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private enum CodingKeys: String, CodingKey { case conversations, nextCursor, unreadTotal }
|
|
250
|
+
|
|
251
|
+
public init(from decoder: Decoder) throws {
|
|
252
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
253
|
+
conversations = c.lenient(LossyArray<ConversationSummary>.self, .conversations)?.items ?? []
|
|
254
|
+
nextCursor = c.lenient(String.self, .nextCursor)
|
|
255
|
+
unreadTotal = c.lenient(Int.self, .unreadTotal) ?? 0
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public struct SessionInfo: Codable, Equatable, Sendable {
|
|
260
|
+
public var sessionToken: String
|
|
261
|
+
public var expiresAt: String?
|
|
262
|
+
public var identified: Bool
|
|
263
|
+
public var customerName: String?
|
|
264
|
+
public var versionId: String?
|
|
265
|
+
|
|
266
|
+
public init(sessionToken: String, expiresAt: String? = nil, identified: Bool = false, customerName: String? = nil, versionId: String? = nil) {
|
|
267
|
+
self.sessionToken = sessionToken
|
|
268
|
+
self.expiresAt = expiresAt
|
|
269
|
+
self.identified = identified
|
|
270
|
+
self.customerName = customerName
|
|
271
|
+
self.versionId = versionId
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private enum CodingKeys: String, CodingKey { case sessionToken, expiresAt, customer, versionId }
|
|
275
|
+
private enum CustomerKeys: String, CodingKey { case identified, name }
|
|
276
|
+
|
|
277
|
+
public init(from decoder: Decoder) throws {
|
|
278
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
279
|
+
guard let token = c.lenient(String.self, .sessionToken), !token.isEmpty else {
|
|
280
|
+
throw DecodingError.keyNotFound(CodingKeys.sessionToken, DecodingError.Context(codingPath: [], debugDescription: "sessionToken"))
|
|
281
|
+
}
|
|
282
|
+
sessionToken = token
|
|
283
|
+
expiresAt = c.lenient(String.self, .expiresAt)
|
|
284
|
+
versionId = c.lenient(String.self, .versionId)
|
|
285
|
+
if let customer = try? c.nestedContainer(keyedBy: CustomerKeys.self, forKey: .customer) {
|
|
286
|
+
identified = customer.lenient(Bool.self, .identified) ?? false
|
|
287
|
+
customerName = customer.lenient(String.self, .name)
|
|
288
|
+
} else {
|
|
289
|
+
identified = false
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public func encode(to encoder: Encoder) throws {
|
|
294
|
+
var c = encoder.container(keyedBy: CodingKeys.self)
|
|
295
|
+
try c.encode(sessionToken, forKey: .sessionToken)
|
|
296
|
+
try c.encodeIfPresent(expiresAt, forKey: .expiresAt)
|
|
297
|
+
try c.encodeIfPresent(versionId, forKey: .versionId)
|
|
298
|
+
var customer = c.nestedContainer(keyedBy: CustomerKeys.self, forKey: .customer)
|
|
299
|
+
try customer.encode(identified, forKey: .identified)
|
|
300
|
+
try customer.encodeIfPresent(customerName, forKey: .name)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public struct SendReceipt: Decodable, Equatable, Sendable {
|
|
305
|
+
public var clientMessageId: String?
|
|
306
|
+
public var accepted: Bool
|
|
307
|
+
public var conversationId: String?
|
|
308
|
+
|
|
309
|
+
private enum CodingKeys: String, CodingKey { case clientMessageId, accepted, conversationId }
|
|
310
|
+
|
|
311
|
+
public init(clientMessageId: String?, accepted: Bool, conversationId: String?) {
|
|
312
|
+
self.clientMessageId = clientMessageId
|
|
313
|
+
self.accepted = accepted
|
|
314
|
+
self.conversationId = conversationId
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public init(from decoder: Decoder) throws {
|
|
318
|
+
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
319
|
+
clientMessageId = c.lenient(String.self, .clientMessageId)
|
|
320
|
+
accepted = c.lenient(Bool.self, .accepted) ?? true
|
|
321
|
+
conversationId = c.lenient(String.self, .conversationId)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// One event from the SSE stream.
|
|
326
|
+
public enum RealtimeEvent: Equatable, Sendable {
|
|
327
|
+
case ready(conversationId: String?)
|
|
328
|
+
case message(ChatMessage, conversationId: String?)
|
|
329
|
+
case typing(active: Bool, agentName: String?)
|
|
330
|
+
case agentJoined(agentName: String?, avatarUrl: String?)
|
|
331
|
+
case unread(Int)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public enum RealtimeEventDecoder {
|
|
335
|
+
/// Nil for an unknown event name or bad data. Never throws.
|
|
336
|
+
public static func decode(_ event: SSEEvent) -> RealtimeEvent? {
|
|
337
|
+
guard let name = event.name, let data = event.data.data(using: .utf8) else { return nil }
|
|
338
|
+
guard let object = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else { return nil }
|
|
339
|
+
switch name {
|
|
340
|
+
case "ready":
|
|
341
|
+
return .ready(conversationId: object["conversationId"] as? String)
|
|
342
|
+
case "message":
|
|
343
|
+
guard let message = try? JSONDecoder().decode(ChatMessage.self, from: data), message.isUsable else { return nil }
|
|
344
|
+
return .message(message, conversationId: object["conversationId"] as? String)
|
|
345
|
+
case "typing":
|
|
346
|
+
return .typing(active: (object["active"] as? Bool) ?? false, agentName: object["agentName"] as? String)
|
|
347
|
+
case "agent_joined":
|
|
348
|
+
return .agentJoined(agentName: object["agentName"] as? String, avatarUrl: object["agentAvatarUrl"] as? String)
|
|
349
|
+
case "unread":
|
|
350
|
+
guard let total = object["unreadTotal"] as? Int else { return nil }
|
|
351
|
+
return .unread(total)
|
|
352
|
+
default:
|
|
353
|
+
return nil
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
extension String {
|
|
359
|
+
/// The longest start of the string that fits in `limit` UTF-16 units (the server counts these). Never cuts a character in two.
|
|
360
|
+
func clipped(toUTF16 limit: Int) -> String {
|
|
361
|
+
guard limit > 0 else { return "" }
|
|
362
|
+
if utf16.count <= limit { return self }
|
|
363
|
+
var used = 0
|
|
364
|
+
var result = ""
|
|
365
|
+
for character in self {
|
|
366
|
+
let size = character.utf16.count
|
|
367
|
+
if used + size > limit { break }
|
|
368
|
+
used += size
|
|
369
|
+
result.append(character)
|
|
370
|
+
}
|
|
371
|
+
return result
|
|
372
|
+
}
|
|
373
|
+
}
|