@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.
Files changed (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +239 -2
  3. package/ZellioChatReactNative.podspec +30 -0
  4. package/android/build.gradle +62 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatModule.kt +214 -0
  7. package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatPackage.kt +25 -0
  8. package/app.plugin.js +4 -0
  9. package/ios/RNZellioChat.h +5 -0
  10. package/ios/RNZellioChat.mm +116 -0
  11. package/ios/ZCRNBridge.swift +217 -0
  12. package/ios/vendor/resources/PrivacyInfo.xcprivacy +99 -0
  13. package/ios/vendor/swift/ZellioChat/Model/ChatModel.swift +261 -0
  14. package/ios/vendor/swift/ZellioChat/Platform/Platform.swift +124 -0
  15. package/ios/vendor/swift/ZellioChat/Platform/URLSessionTransport.swift +175 -0
  16. package/ios/vendor/swift/ZellioChat/Preview/Previews.swift +62 -0
  17. package/ios/vendor/swift/ZellioChat/Public/InAppBanner.swift +155 -0
  18. package/ios/vendor/swift/ZellioChat/Public/PublicTypes.swift +141 -0
  19. package/ios/vendor/swift/ZellioChat/Public/Runtime.swift +406 -0
  20. package/ios/vendor/swift/ZellioChat/Public/ZellioChat.swift +209 -0
  21. package/ios/vendor/swift/ZellioChat/Theme/ChatTheme.swift +185 -0
  22. package/ios/vendor/swift/ZellioChat/Thread/AttachmentPicker.swift +354 -0
  23. package/ios/vendor/swift/ZellioChat/Thread/ComposerBar.swift +355 -0
  24. package/ios/vendor/swift/ZellioChat/Thread/ThreadCells.swift +776 -0
  25. package/ios/vendor/swift/ZellioChat/Thread/ThreadViewController.swift +703 -0
  26. package/ios/vendor/swift/ZellioChat/Views/ChatRootView.swift +154 -0
  27. package/ios/vendor/swift/ZellioChat/Views/Components.swift +335 -0
  28. package/ios/vendor/swift/ZellioChat/Views/HomeView.swift +413 -0
  29. package/ios/vendor/swift/ZellioChatCore/ChatModels.swift +373 -0
  30. package/ios/vendor/swift/ZellioChatCore/ConfigModels.swift +389 -0
  31. package/ios/vendor/swift/ZellioChatCore/Engine.swift +325 -0
  32. package/ios/vendor/swift/ZellioChatCore/Errors.swift +158 -0
  33. package/ios/vendor/swift/ZellioChatCore/Generated/ChatStringTable.swift +125 -0
  34. package/ios/vendor/swift/ZellioChatCore/Outbox.swift +223 -0
  35. package/ios/vendor/swift/ZellioChatCore/Push.swift +74 -0
  36. package/ios/vendor/swift/ZellioChatCore/Realtime.swift +328 -0
  37. package/ios/vendor/swift/ZellioChatCore/Session.swift +210 -0
  38. package/ios/vendor/swift/ZellioChatCore/Storage.swift +201 -0
  39. package/ios/vendor/swift/ZellioChatCore/Strings.swift +104 -0
  40. package/ios/vendor/swift/ZellioChatCore/Theme.swift +238 -0
  41. package/ios/vendor/swift/ZellioChatCore/ThreadController.swift +319 -0
  42. package/ios/vendor/swift/ZellioChatCore/Timeline.swift +157 -0
  43. package/ios/vendor/swift/ZellioChatCore/Transport.swift +236 -0
  44. package/ios/vendor/swift/ZellioChatCore/Util.swift +137 -0
  45. package/lib/NativeZellioChat.d.ts +34 -0
  46. package/lib/NativeZellioChat.js +4 -0
  47. package/lib/index.d.ts +37 -0
  48. package/lib/index.js +241 -0
  49. package/lib/types.d.ts +54 -0
  50. package/lib/types.js +2 -0
  51. package/lib/version.d.ts +4 -0
  52. package/lib/version.js +7 -0
  53. package/package.json +75 -4
  54. package/plugin/build/index.js +73 -0
  55. package/src/NativeZellioChat.ts +30 -0
  56. package/src/index.ts +237 -0
  57. package/src/types.ts +56 -0
  58. package/src/version.ts +5 -0
@@ -0,0 +1,325 @@
1
+ import Foundation
2
+
3
+ public struct EngineOptions: Sendable {
4
+ public var baseURL: URL
5
+ public var appKey: String
6
+ /// nil follows the device.
7
+ public var localeOption: ChatLocale?
8
+ public var deviceLanguageCode: String?
9
+ public var wrapper: String?
10
+ public var appVersion: String?
11
+
12
+ public init(baseURL: URL, appKey: String, localeOption: ChatLocale? = nil, deviceLanguageCode: String? = nil, wrapper: String? = nil, appVersion: String? = nil) {
13
+ self.baseURL = baseURL
14
+ self.appKey = appKey
15
+ self.localeOption = localeOption
16
+ self.deviceLanguageCode = deviceLanguageCode
17
+ self.wrapper = wrapper
18
+ self.appVersion = appVersion
19
+ }
20
+ }
21
+
22
+ public enum HubEvent: Sendable {
23
+ case realtime(RealtimeEvent)
24
+ case reconnected
25
+ case outbox(Outbox.Update)
26
+ /// Logout or a customer switch removed the local data. Caches outside the storage must go too.
27
+ case localDataWiped
28
+ }
29
+
30
+ /// Ties the parts together: config, consent, session, conversations, outbox, stream, push, logout.
31
+ /// Every method is safe from any thread. Nothing here blocks the caller.
32
+ public final class ChatEngine: @unchecked Sendable {
33
+ public let options: EngineOptions
34
+ public let api: ChatAPI
35
+ public let storage: StorageBackend
36
+ public let secrets: SecretStore
37
+ public let clock: ChatClock
38
+ public let installId: String
39
+ public let queue: OfflineQueue
40
+ public let outbox: Outbox
41
+ public let session: SessionCoordinator
42
+ public let unread = UnreadTracker()
43
+ public let consentStore: ConsentStore
44
+ public let drafts: DraftStore
45
+ private let configCache: ConfigCache
46
+
47
+ private struct State {
48
+ var config: ChatConfig?
49
+ var freshConfig = false
50
+ var context: ChatContext?
51
+ var localeOption: ChatLocale?
52
+ var listeners: [Int: @Sendable (HubEvent) -> Void] = [:]
53
+ var nextListener = 0
54
+ var streamConversationId: String?
55
+ var realtime: RealtimeClient?
56
+ var pushToken: PushRegistration?
57
+ }
58
+ private let state = Locked(State())
59
+
60
+ public struct PushRegistration: Codable, Equatable, Sendable {
61
+ public var token: String
62
+ public var environment: String?
63
+ }
64
+
65
+ public init(options: EngineOptions, transport: HTTPTransport, secrets: SecretStore, storage: StorageBackend, clock: ChatClock = SystemClock(), random: RandomSource = SystemRandom()) {
66
+ let install = InstallId.load(storage)
67
+ let api = ChatAPI(config: ChatAPIConfig(baseURL: options.baseURL, appKey: options.appKey, wrapper: options.wrapper), transport: transport)
68
+ let queue = OfflineQueue(storage: storage, clock: clock)
69
+ let box = Locked<ChatEngine?>(nil)
70
+ let sessionRef = Locked<SessionCoordinator?>(nil)
71
+ let outbox = Outbox(queue: queue, clock: clock, random: random) { item in
72
+ guard let coordinator = sessionRef.snapshot else { throw ChatError.network }
73
+ return try await coordinator.perform { token in
74
+ try await api.sendMessage(text: item.text, clientMessageId: item.clientMessageId, attachmentIds: item.attachmentIds, context: item.context, token: token)
75
+ }
76
+ }
77
+ let session = SessionCoordinator(
78
+ api: api, secrets: secrets, storage: storage, clock: clock, installId: install,
79
+ locale: { box.snapshot?.locale() ?? .en },
80
+ allowed: { try await box.snapshot?.requireSessionAllowed() },
81
+ onWipe: { box.snapshot?.didWipe() },
82
+ onSessionStarted: {
83
+ guard let engine = box.snapshot else { return }
84
+ Task { await engine.registerPushIfPossible() }
85
+ }
86
+ )
87
+ self.options = options
88
+ self.storage = storage
89
+ self.secrets = secrets
90
+ self.clock = clock
91
+ self.installId = install
92
+ self.api = api
93
+ self.queue = queue
94
+ self.outbox = outbox
95
+ self.session = session
96
+ self.consentStore = ConsentStore(storage: storage)
97
+ self.drafts = DraftStore(storage: storage)
98
+ self.configCache = ConfigCache(storage: storage)
99
+ sessionRef.with { $0 = session }
100
+ box.with { $0 = self }
101
+ state.with { s in
102
+ s.localeOption = options.localeOption
103
+ s.config = self.configCache.load()
104
+ s.pushToken = storage.load(PushRegistration.self, StorageName.push)
105
+ }
106
+ let engine = self
107
+ Task { await engine.outbox.setListener { [weak engine] update in engine?.emit(.outbox(update)) } }
108
+ }
109
+
110
+ // MARK: Config, locale, consent
111
+
112
+ public var cachedConfig: ChatConfig? { state.with { $0.config } }
113
+ public var isConfigFresh: Bool { state.with { $0.freshConfig } }
114
+
115
+ /// Reads the config again. On success the new one is saved. Offline keeps the saved one.
116
+ @discardableResult
117
+ public func refreshConfig() async throws -> ChatConfig {
118
+ do {
119
+ let body = try await api.fetchConfigBody()
120
+ let config = try ChatConfig.parse(body)
121
+ if config.sdkTooOld { throw ChatError.unavailable }
122
+ configCache.save(body: body)
123
+ state.with { s in
124
+ s.config = config
125
+ s.freshConfig = true
126
+ }
127
+ return config
128
+ } catch {
129
+ throw ErrorPolicy.map(error)
130
+ }
131
+ }
132
+
133
+ public func locale() -> ChatLocale {
134
+ let (config, option) = state.with { ($0.config, $0.localeOption) }
135
+ return LocaleChoice.choose(option: option, policy: config?.snapshot.language, deviceLanguageCode: options.deviceLanguageCode)
136
+ }
137
+
138
+ public func setLocaleOption(_ locale: ChatLocale?) { state.with { $0.localeOption = locale } }
139
+
140
+ public func consentState() -> ConsentState? {
141
+ guard let config = cachedConfig else { return nil }
142
+ return ConsentGate.state(config: config, store: consentStore)
143
+ }
144
+
145
+ public func acceptConsent() {
146
+ guard let config = cachedConfig else { return }
147
+ consentStore.accept(ConsentTerms(behavior: config.snapshot.behavior))
148
+ }
149
+
150
+ /// A session is never started from a saved config alone: the config is read fresh first, then consent is checked.
151
+ func requireSessionAllowed() async throws {
152
+ if !isConfigFresh { try await refreshConfig() }
153
+ guard let config = cachedConfig else { throw ChatError.network }
154
+ if config.sdkTooOld { throw ChatError.unavailable }
155
+ if !ConsentGate.allowsSession(config: config, store: consentStore) { throw ChatError.consentRequired }
156
+ }
157
+
158
+ func didWipe() {
159
+ unread.set(0)
160
+ state.with { s in
161
+ s.streamConversationId = nil
162
+ }
163
+ emit(.localDataWiped)
164
+ }
165
+
166
+ // MARK: Context
167
+
168
+ public func setContext(_ context: ChatContext?) { state.with { $0.context = context?.sanitized() } }
169
+ public var context: ChatContext? { state.with { $0.context } }
170
+
171
+ // MARK: Identity
172
+
173
+ public func identify(_ provider: @escaping IdentityTokenProvider) {
174
+ let engine = self
175
+ Task {
176
+ await engine.session.setProvider(provider)
177
+ // Only when a session exists already. Otherwise the token is used when the session is made.
178
+ try? await engine.session.identifyCurrentSession()
179
+ }
180
+ }
181
+
182
+ public func logout() async {
183
+ await stopRealtime()
184
+ await outbox.cancelAll()
185
+ // Tell the server which push token to forget, then end the session. Local data goes in both cases.
186
+ if let token = await session.token { try? await api.deleteDevice(token: token) }
187
+ await session.logout()
188
+ queue.clear()
189
+ state.with { s in
190
+ // The device token stays: the next session on this phone registers it again.
191
+ s.context = nil
192
+ }
193
+ unread.set(0)
194
+ }
195
+
196
+ // MARK: Conversations, unread
197
+
198
+ public func cachedConversations() -> ConversationsPage? { storage.load(ConversationsPage.self, StorageName.conversations) }
199
+
200
+ public func loadConversations(cursor: String? = nil) async throws -> ConversationsPage {
201
+ do {
202
+ let page = try await session.perform { token in try await self.api.conversations(cursor: cursor, token: token) }
203
+ if cursor == nil { storage.save(page, StorageName.conversations) }
204
+ unread.set(page.unreadTotal)
205
+ return page
206
+ } catch {
207
+ throw ErrorPolicy.map(error)
208
+ }
209
+ }
210
+
211
+ public func refreshUnread() async {
212
+ guard ConsentGate.allowsSession(config: cachedConfig, store: consentStore), await session.token != nil else { return }
213
+ if let total = try? await session.perform({ token in try await self.api.unread(token: token) }) { unread.set(total) }
214
+ }
215
+
216
+ public func markRead(conversationId: String, upTo: String) async {
217
+ if let total = try? await session.perform({ token in try await self.api.markRead(conversationId: conversationId, upTo: upTo, token: token) }) {
218
+ unread.set(total)
219
+ }
220
+ }
221
+
222
+ // MARK: Attachments
223
+
224
+ public func upload(data: Data, fileName: String, mimeType: String, progress: @escaping @Sendable (Double) -> Void) async throws -> Attachment {
225
+ do {
226
+ return try await session.perform { token in
227
+ try await self.api.uploadAttachment(data: data, fileName: fileName, mimeType: mimeType, token: token, progress: progress)
228
+ }
229
+ } catch {
230
+ throw ErrorPolicy.map(error)
231
+ }
232
+ }
233
+
234
+ // MARK: Push
235
+
236
+ public func setPushToken(_ token: String, environment: String?) {
237
+ let registration = PushRegistration(token: token, environment: environment)
238
+ state.with { $0.pushToken = registration }
239
+ storage.save(registration, StorageName.push)
240
+ let engine = self
241
+ Task { await engine.registerPushIfPossible() }
242
+ }
243
+
244
+ /// Sends the stored token when a session exists. It never creates a session by itself.
245
+ public func registerPushIfPossible() async {
246
+ guard let registration = state.with({ $0.pushToken }), await session.token != nil else { return }
247
+ let locale = self.locale()
248
+ _ = try? await session.perform { token in
249
+ try await self.api.registerDevice(provider: "apns", pushToken: registration.token, environment: registration.environment, locale: locale, appVersion: self.options.appVersion, token: token)
250
+ }
251
+ }
252
+
253
+ // MARK: Events hub
254
+
255
+ @discardableResult
256
+ public func addListener(_ listener: @escaping @Sendable (HubEvent) -> Void) -> Int {
257
+ state.with { s in
258
+ s.nextListener += 1
259
+ s.listeners[s.nextListener] = listener
260
+ return s.nextListener
261
+ }
262
+ }
263
+
264
+ public func removeListener(_ token: Int) { state.with { $0.listeners[token] = nil } }
265
+
266
+ func emit(_ event: HubEvent) {
267
+ let listeners: [@Sendable (HubEvent) -> Void] = state.with { s in
268
+ if case .realtime(let inner) = event {
269
+ if case .ready(let id) = inner { s.streamConversationId = id }
270
+ }
271
+ return Array(s.listeners.values)
272
+ }
273
+ if case .realtime(let inner) = event, case .unread(let total) = inner { unread.set(total) }
274
+ for listener in listeners { listener(event) }
275
+ }
276
+
277
+ public var streamConversationId: String? { state.with { $0.streamConversationId } }
278
+
279
+ // MARK: Realtime and lifecycle
280
+
281
+ public func startRealtime() async {
282
+ let existing = state.with { $0.realtime }
283
+ if let existing = existing { await existing.start(); return }
284
+ let client = RealtimeClient(
285
+ api: api, clock: clock,
286
+ tokenSource: { [weak self] in
287
+ guard let self = self else { throw ChatError.cancelled }
288
+ return try await self.session.ensureSession()
289
+ },
290
+ onEvent: { [weak self] event in self?.emit(.realtime(event)) },
291
+ catchUp: { [weak self] in
292
+ guard let self = self else { return }
293
+ await self.refreshUnread()
294
+ self.emit(.reconnected)
295
+ },
296
+ onFailure: { [weak self] error in
297
+ guard let self = self else { return }
298
+ if ErrorPolicy.reaction(for: error) == .recreateSessionThenRetry { await self.session.invalidateToken() }
299
+ }
300
+ )
301
+ state.with { $0.realtime = client }
302
+ await client.start()
303
+ }
304
+
305
+ public func stopRealtime() async {
306
+ if let client = state.with({ $0.realtime }) { await client.stop() }
307
+ }
308
+
309
+ /// The app went to the background: close the stream.
310
+ public func enteredBackground() async {
311
+ if let client = state.with({ $0.realtime }) { await client.enteredBackground() }
312
+ }
313
+
314
+ /// The app came back: read unread, read newer messages, reopen the stream. Also sends the queue.
315
+ public func enteredForeground() async {
316
+ if let client = state.with({ $0.realtime }) { await client.enteredForeground() } else { await refreshUnread() }
317
+ await outbox.kick()
318
+ }
319
+
320
+ public func networkReturned() async { await outbox.networkReturned() }
321
+
322
+ public func makeThread(conversationId: String?) -> ThreadController {
323
+ ThreadController(engine: self, conversationId: conversationId)
324
+ }
325
+ }
@@ -0,0 +1,158 @@
1
+ import Foundation
2
+
3
+ /// The stable error codes of contract v1.
4
+ public enum ChatErrorCode: String, Equatable, Sendable {
5
+ case appKeyInvalid = "CHAT_APP_KEY_INVALID"
6
+ case appDisabled = "CHAT_APP_DISABLED"
7
+ case entitlementDenied = "CHAT_ENTITLEMENT_DENIED"
8
+ case sessionInvalid = "CHAT_SESSION_INVALID"
9
+ case sessionRequired = "CHAT_SESSION_REQUIRED"
10
+ case identityInvalid = "CHAT_IDENTITY_INVALID"
11
+ case guestsDisabled = "CHAT_GUESTS_DISABLED"
12
+ case conversationNotFound = "CHAT_CONVERSATION_NOT_FOUND"
13
+ case fileTooLarge = "CHAT_FILE_TOO_LARGE"
14
+ case fileTypeDenied = "CHAT_FILE_TYPE_DENIED"
15
+ case filesDisabled = "CHAT_FILES_DISABLED"
16
+ case attachmentNotFound = "CHAT_ATTACHMENT_NOT_FOUND"
17
+ case rateLimited = "CHAT_RATE_LIMITED"
18
+ case sdkTooOld = "CHAT_SDK_TOO_OLD"
19
+ case siteNotFound = "CHAT_SITE_NOT_FOUND"
20
+ }
21
+
22
+ /// What the SDK does for an error code.
23
+ public enum ErrorReaction: Equatable, Sendable {
24
+ case stopUnavailable
25
+ case recreateSessionThenRetry
26
+ case refreshIdentityOnce
27
+ case showLoginNeeded
28
+ case reloadConversationList
29
+ case showFileTooLarge
30
+ case showFileTypeDenied
31
+ case hideAttach
32
+ case backOff
33
+ case none
34
+ }
35
+
36
+ /// A failure of one API call.
37
+ public enum ChatAPIError: Error, Equatable, Sendable {
38
+ case http(status: Int, code: String?, retryAfter: TimeInterval?)
39
+ case network
40
+ case timeout
41
+ case decoding
42
+ case cancelled
43
+
44
+ public var status: Int? {
45
+ if case .http(let status, _, _) = self { return status }
46
+ return nil
47
+ }
48
+
49
+ public var errorCode: ChatErrorCode? {
50
+ if case .http(_, let code, _) = self, let code = code { return ChatErrorCode(rawValue: code) }
51
+ return nil
52
+ }
53
+ }
54
+
55
+ /// The error the app-facing layers see. Mapped to a screen state.
56
+ public enum ChatError: Error, Equatable, Sendable {
57
+ case unavailable
58
+ case loginNeeded
59
+ case rateLimited(retryAfter: TimeInterval?)
60
+ case fileTooLarge
61
+ case fileTypeDenied
62
+ case filesDisabled
63
+ case conversationNotFound
64
+ case consentRequired
65
+ case network
66
+ case server(status: Int)
67
+ case decoding
68
+ case cancelled
69
+
70
+ /// A short code for the public `error(code)` event. No message text.
71
+ public var eventCode: String {
72
+ switch self {
73
+ case .unavailable: return "unavailable"
74
+ case .loginNeeded: return "login_needed"
75
+ case .rateLimited: return "rate_limited"
76
+ case .fileTooLarge: return "file_too_large"
77
+ case .fileTypeDenied: return "file_type_denied"
78
+ case .filesDisabled: return "files_disabled"
79
+ case .conversationNotFound: return "conversation_not_found"
80
+ case .consentRequired: return "consent_required"
81
+ case .network: return "network"
82
+ case .server: return "server"
83
+ case .decoding: return "decoding"
84
+ case .cancelled: return "cancelled"
85
+ }
86
+ }
87
+ }
88
+
89
+ public enum ErrorPolicy {
90
+ /// Reads the code from `{ "code": ... }` or `{ "error": { "code": ... } }`.
91
+ public static func parseCode(from body: Data?) -> String? {
92
+ guard let body = body, !body.isEmpty,
93
+ let root = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] else { return nil }
94
+ if let code = root["code"] as? String { return code }
95
+ if let error = root["error"] as? [String: Any], let code = error["code"] as? String { return code }
96
+ return nil
97
+ }
98
+
99
+ public static func reaction(forCode code: String?) -> ErrorReaction {
100
+ guard let code = code, let known = ChatErrorCode(rawValue: code) else { return .none }
101
+ switch known {
102
+ case .appKeyInvalid, .appDisabled, .entitlementDenied, .sdkTooOld, .siteNotFound: return .stopUnavailable
103
+ case .sessionInvalid, .sessionRequired: return .recreateSessionThenRetry
104
+ case .identityInvalid: return .refreshIdentityOnce
105
+ case .guestsDisabled: return .showLoginNeeded
106
+ case .conversationNotFound: return .reloadConversationList
107
+ case .fileTooLarge: return .showFileTooLarge
108
+ case .fileTypeDenied: return .showFileTypeDenied
109
+ case .filesDisabled: return .hideAttach
110
+ // The message fails for good. The customer attaches the file again.
111
+ case .attachmentNotFound: return .none
112
+ case .rateLimited: return .backOff
113
+ }
114
+ }
115
+
116
+ /// A known code decides. An unknown code falls back to the HTTP status. No code (a proxy, for example) gives `.none`, except a 429.
117
+ public static func reaction(status: Int, code: String?) -> ErrorReaction {
118
+ guard let code = code else { return status == 429 ? .backOff : .none }
119
+ if ChatErrorCode(rawValue: code) != nil { return reaction(forCode: code) }
120
+ switch status {
121
+ case 401: return .recreateSessionThenRetry
122
+ case 403: return .stopUnavailable
123
+ case 429: return .backOff
124
+ default: return .none
125
+ }
126
+ }
127
+
128
+ public static func reaction(for error: Error) -> ErrorReaction {
129
+ guard let api = error as? ChatAPIError, case .http(let status, let code, _) = api else { return .none }
130
+ return reaction(status: status, code: code)
131
+ }
132
+
133
+ /// Maps any error to the app-facing error.
134
+ public static func map(_ error: Error) -> ChatError {
135
+ if let chat = error as? ChatError { return chat }
136
+ if error is CancellationError { return .cancelled }
137
+ guard let api = error as? ChatAPIError else { return .network }
138
+ switch api {
139
+ case .network, .timeout: return .network
140
+ case .decoding: return .decoding
141
+ case .cancelled: return .cancelled
142
+ case .http(let status, let code, let retryAfter):
143
+ switch reaction(status: status, code: code) {
144
+ case .stopUnavailable: return .unavailable
145
+ case .showLoginNeeded, .refreshIdentityOnce: return .loginNeeded
146
+ case .reloadConversationList: return .conversationNotFound
147
+ case .showFileTooLarge: return .fileTooLarge
148
+ case .showFileTypeDenied: return .fileTypeDenied
149
+ case .hideAttach: return .filesDisabled
150
+ case .backOff: return .rateLimited(retryAfter: retryAfter)
151
+ case .recreateSessionThenRetry: return .loginNeeded
152
+ case .none:
153
+ if status == 429 { return .rateLimited(retryAfter: retryAfter) }
154
+ return .server(status: status)
155
+ }
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,125 @@
1
+ // GENERATED by scripts/generate-strings.mjs from sdks/design/strings.json. Do not edit by hand.
2
+
3
+ enum ChatStringTable {
4
+ static let en: [String: String] = [
5
+ "a11y.team": "Support team",
6
+ "a11y.unread": "Unread",
7
+ "a11y.you": "You",
8
+ "attachment.cancel": "Cancel upload",
9
+ "attachment.close": "Close",
10
+ "attachment.failed": "We could not upload this file. Try again.",
11
+ "attachment.file": "File",
12
+ "attachment.photo": "Photo",
13
+ "attachment.tooLarge": "This file is too large. The limit is 10 MB.",
14
+ "attachment.typeDenied": "You can send photos and PDF files only.",
15
+ "banner.offline": "We are away right now. Leave a message and we will reply when we are back.",
16
+ "banner.online": "Our team is here to help",
17
+ "banner.outsideHours": "Outside support hours",
18
+ "banner.reconnecting": "Reconnecting…",
19
+ "common.back": "Back",
20
+ "common.close": "Close chat",
21
+ "common.copied": "Copied",
22
+ "common.copy": "Copy",
23
+ "common.poweredBy": "Powered by Zellio",
24
+ "composer.attach": "Attach a file",
25
+ "composer.camera": "Camera",
26
+ "composer.cameraDenied": "Allow camera access in Settings to take a photo.",
27
+ "composer.files": "File",
28
+ "composer.openSettings": "Open Settings",
29
+ "composer.photos": "Photo library",
30
+ "composer.placeholder": "Write your message…",
31
+ "composer.send": "Send",
32
+ "consent.accept": "Accept and start",
33
+ "consent.decline": "Not now",
34
+ "consent.default": "To help you, we store your messages and share them with our support team.",
35
+ "consent.privacy": "Privacy policy",
36
+ "delivery.failed": "Not sent",
37
+ "delivery.retry": "Try again",
38
+ "delivery.sending": "Sending…",
39
+ "delivery.sent": "Sent",
40
+ "delivery.waiting": "Sends when you are back online",
41
+ "home.fallbackHeading": "How can we help?",
42
+ "home.recent": "Recent conversations",
43
+ "home.seeAll": "See all",
44
+ "home.talkToTeam": "Talk to our team",
45
+ "list.empty": "You have no conversations yet.",
46
+ "list.new": "New conversation",
47
+ "list.title": "Conversations",
48
+ "push.newMessage": "New message",
49
+ "state.loadFailed": "We could not load your conversation. Try again.",
50
+ "state.loading": "Loading…",
51
+ "state.loginNeeded": "Log in to the app to chat with us.",
52
+ "state.rateLimited": "You are sending messages too fast. Please wait a few minutes, then try again.",
53
+ "state.retry": "Try again",
54
+ "state.sendFailed": "We could not send your message. Try again.",
55
+ "state.unavailable": "Chat is not available right now.",
56
+ "thread.greeting": "Hi! Tell us what you need help with.",
57
+ "thread.loadingOlder": "Loading earlier messages…",
58
+ "thread.newMessage": "New message",
59
+ "thread.title": "Your conversation",
60
+ "thread.today": "Today",
61
+ "thread.typing": "Our team is typing…",
62
+ "thread.yesterday": "Yesterday",
63
+ ]
64
+
65
+ static let ar: [String: String] = [
66
+ "a11y.team": "فريق الدعم",
67
+ "a11y.unread": "مش مقروءة",
68
+ "a11y.you": "إنت",
69
+ "attachment.cancel": "إلغاء الرفع",
70
+ "attachment.close": "إغلاق",
71
+ "attachment.failed": "معرفناش نرفع الملف. جرّب تاني.",
72
+ "attachment.file": "ملف",
73
+ "attachment.photo": "صورة",
74
+ "attachment.tooLarge": "الملف كبير أوي. الحد الأقصى 10 ميجا.",
75
+ "attachment.typeDenied": "تقدر تبعت صور وملفات PDF بس.",
76
+ "banner.offline": "إحنا مش متاحين دلوقتي. سيب رسالتك وهنرد عليك أول ما نرجع.",
77
+ "banner.online": "فريقنا موجود عشان يساعدك",
78
+ "banner.outsideHours": "خارج مواعيد العمل",
79
+ "banner.reconnecting": "بنحاول نرجّع الاتصال…",
80
+ "common.back": "رجوع",
81
+ "common.close": "إغلاق الشات",
82
+ "common.copied": "اتنسخ",
83
+ "common.copy": "نسخ",
84
+ "common.poweredBy": "بدعم من Zellio",
85
+ "composer.attach": "إرفاق ملف",
86
+ "composer.camera": "الكاميرا",
87
+ "composer.cameraDenied": "اسمح باستخدام الكاميرا من الإعدادات عشان تصوّر.",
88
+ "composer.files": "ملف",
89
+ "composer.openSettings": "افتح الإعدادات",
90
+ "composer.photos": "الصور",
91
+ "composer.placeholder": "اكتب رسالتك…",
92
+ "composer.send": "إرسال",
93
+ "consent.accept": "موافق، يلا نبدأ",
94
+ "consent.decline": "مش دلوقتي",
95
+ "consent.default": "عشان نقدر نساعدك، هنحفظ رسايلك ونشاركها مع فريق الدعم.",
96
+ "consent.privacy": "سياسة الخصوصية",
97
+ "delivery.failed": "ماتبعتتش",
98
+ "delivery.retry": "جرّب تاني",
99
+ "delivery.sending": "جارٍ الإرسال…",
100
+ "delivery.sent": "اتبعتت",
101
+ "delivery.waiting": "هتتبعت أول ما النت يرجع",
102
+ "home.fallbackHeading": "إزاي نقدر نساعدك؟",
103
+ "home.recent": "آخر المحادثات",
104
+ "home.seeAll": "شوف الكل",
105
+ "home.talkToTeam": "كلّم فريقنا",
106
+ "list.empty": "لسه مفيش محادثات.",
107
+ "list.new": "محادثة جديدة",
108
+ "list.title": "المحادثات",
109
+ "push.newMessage": "رسالة جديدة",
110
+ "state.loadFailed": "معرفناش نحمّل المحادثة. جرّب تاني.",
111
+ "state.loading": "جارٍ التحميل…",
112
+ "state.loginNeeded": "سجّل دخولك في التطبيق عشان تكلّمنا.",
113
+ "state.rateLimited": "بعت رسايل كتير ورا بعض. استنى كام دقيقة وجرّب تاني.",
114
+ "state.retry": "جرّب تاني",
115
+ "state.sendFailed": "معرفناش نبعت الرسالة. جرّب تاني.",
116
+ "state.unavailable": "الشات مش متاح دلوقتي.",
117
+ "thread.greeting": "أهلاً! قولّنا محتاج إيه وهنساعدك.",
118
+ "thread.loadingOlder": "بنحمّل الرسايل القديمة…",
119
+ "thread.newMessage": "رسالة جديدة",
120
+ "thread.title": "محادثتك",
121
+ "thread.today": "النهاردة",
122
+ "thread.typing": "الفريق بيكتب…",
123
+ "thread.yesterday": "إمبارح",
124
+ ]
125
+ }