@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,355 @@
1
+ #if canImport(SwiftUI) && canImport(UIKit)
2
+ import UIKit
3
+
4
+ /// One attachment on its way: a thumbnail with a progress ring. Tap cancels or removes it.
5
+ final class PendingChip: UIControl {
6
+ enum State { case uploading(Double), ready, failed }
7
+
8
+ let id: String
9
+ private let image = UIImageView()
10
+ private let ring = CAShapeLayer()
11
+ private let track = CAShapeLayer()
12
+ private let overlay = UIView()
13
+ private let icon = UIImageView()
14
+
15
+ init(id: String, thumbnail: UIImage?, fileName: String) {
16
+ self.id = id
17
+ super.init(frame: .zero)
18
+ translatesAutoresizingMaskIntoConstraints = false
19
+ layer.cornerRadius = 12
20
+ clipsToBounds = true
21
+ backgroundColor = UIColor.gray.withAlphaComponent(0.25)
22
+ image.image = thumbnail
23
+ image.contentMode = .scaleAspectFill
24
+ image.clipsToBounds = true
25
+ image.translatesAutoresizingMaskIntoConstraints = false
26
+ addSubview(image)
27
+ overlay.backgroundColor = UIColor.black.withAlphaComponent(0.4)
28
+ overlay.isUserInteractionEnabled = false
29
+ overlay.translatesAutoresizingMaskIntoConstraints = false
30
+ addSubview(overlay)
31
+ icon.tintColor = .white
32
+ icon.contentMode = .scaleAspectFit
33
+ icon.translatesAutoresizingMaskIntoConstraints = false
34
+ addSubview(icon)
35
+ if thumbnail == nil { icon.image = UIImage(systemName: "doc") }
36
+ NSLayoutConstraint.activate([
37
+ widthAnchor.constraint(equalToConstant: 56), heightAnchor.constraint(equalToConstant: 56),
38
+ image.leadingAnchor.constraint(equalTo: leadingAnchor), image.trailingAnchor.constraint(equalTo: trailingAnchor),
39
+ image.topAnchor.constraint(equalTo: topAnchor), image.bottomAnchor.constraint(equalTo: bottomAnchor),
40
+ overlay.leadingAnchor.constraint(equalTo: leadingAnchor), overlay.trailingAnchor.constraint(equalTo: trailingAnchor),
41
+ overlay.topAnchor.constraint(equalTo: topAnchor), overlay.bottomAnchor.constraint(equalTo: bottomAnchor),
42
+ icon.centerXAnchor.constraint(equalTo: centerXAnchor), icon.centerYAnchor.constraint(equalTo: centerYAnchor),
43
+ icon.widthAnchor.constraint(equalToConstant: 22), icon.heightAnchor.constraint(equalToConstant: 22),
44
+ ])
45
+ for shape in [track, ring] {
46
+ shape.fillColor = UIColor.clear.cgColor
47
+ shape.lineWidth = 3
48
+ shape.lineCap = .round
49
+ layer.addSublayer(shape)
50
+ }
51
+ track.strokeColor = UIColor.white.withAlphaComponent(0.3).cgColor
52
+ ring.strokeColor = UIColor.white.cgColor
53
+ ring.strokeEnd = 0
54
+ isAccessibilityElement = true
55
+ accessibilityLabel = fileName
56
+ accessibilityTraits = .button
57
+ }
58
+
59
+ required init?(coder: NSCoder) { nil }
60
+
61
+ override func layoutSubviews() {
62
+ super.layoutSubviews()
63
+ let path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: 14, startAngle: -.pi / 2, endAngle: 1.5 * .pi, clockwise: true).cgPath
64
+ track.path = path
65
+ ring.path = path
66
+ }
67
+
68
+ func apply(_ state: State) {
69
+ switch state {
70
+ case .uploading(let progress):
71
+ overlay.isHidden = false
72
+ track.isHidden = false
73
+ ring.isHidden = false
74
+ ring.strokeEnd = CGFloat(Swift.max(0.05, Swift.min(1, progress)))
75
+ icon.image = UIImage(systemName: "xmark")
76
+ icon.isHidden = false
77
+ case .ready:
78
+ overlay.isHidden = true
79
+ track.isHidden = true
80
+ ring.isHidden = true
81
+ icon.isHidden = true
82
+ case .failed:
83
+ overlay.isHidden = false
84
+ track.isHidden = true
85
+ ring.isHidden = true
86
+ icon.image = UIImage(systemName: "exclamationmark.circle")
87
+ icon.isHidden = false
88
+ }
89
+ }
90
+ }
91
+
92
+ /// The composer. On a phone it is the thread's input accessory view, so the keyboard dismisses interactively by drag.
93
+ final class ComposerBar: UIView, UITextViewDelegate {
94
+ var onSend: ((String) -> Void)?
95
+ var onAttach: (() -> Void)?
96
+ var onTextChange: ((String) -> Void)?
97
+ var onHeightChange: (() -> Void)?
98
+
99
+ private let topLine = UIView()
100
+ private let attachButton = UIButton(type: .system)
101
+ private let pill = UIView()
102
+ private let textView = UITextView()
103
+ private let placeholder = UILabel()
104
+ private let sendButton = UIButton(type: .system)
105
+ private let chips = UIStackView()
106
+ private let chipScroll = UIScrollView()
107
+ private let statusLabel = UILabel()
108
+ private let column = UIStackView()
109
+ private var chipHeight: NSLayoutConstraint?
110
+ private var theme: ChatTheme = .fallback
111
+ private var canAttach = true
112
+ private var inputRow: UIStackView?
113
+ private(set) var hasPendingAttachments = false
114
+ private var textHeight: CGFloat = 36
115
+ private var textHeightConstraint: NSLayoutConstraint?
116
+ private var pillMinHeight: CGFloat = 44
117
+
118
+ init() {
119
+ super.init(frame: .zero)
120
+ autoresizingMask = .flexibleHeight
121
+ build()
122
+ }
123
+
124
+ required init?(coder: NSCoder) { nil }
125
+
126
+ var text: String {
127
+ get { textView.text ?? "" }
128
+ set { textView.text = newValue; refresh() }
129
+ }
130
+
131
+ func focus() { textView.becomeFirstResponder() }
132
+
133
+ private func build() {
134
+ topLine.translatesAutoresizingMaskIntoConstraints = false
135
+ addSubview(topLine)
136
+
137
+ attachButton.setImage(UIImage(systemName: "paperclip"), for: .normal)
138
+ attachButton.addTarget(self, action: #selector(attachTapped), for: .touchUpInside)
139
+ attachButton.translatesAutoresizingMaskIntoConstraints = false
140
+
141
+ pill.layer.cornerRadius = 22
142
+ pill.layer.borderWidth = 1
143
+ pill.translatesAutoresizingMaskIntoConstraints = false
144
+
145
+ textView.delegate = self
146
+ textView.isScrollEnabled = false
147
+ textView.backgroundColor = .clear
148
+ textView.textContainerInset = UIEdgeInsets(top: 7, left: 0, bottom: 7, right: 0)
149
+ textView.textContainer.lineFragmentPadding = 0
150
+ textView.adjustsFontForContentSizeCategory = true
151
+ textView.returnKeyType = .default
152
+ textView.translatesAutoresizingMaskIntoConstraints = false
153
+
154
+ placeholder.isUserInteractionEnabled = false
155
+ placeholder.translatesAutoresizingMaskIntoConstraints = false
156
+
157
+ sendButton.setImage(UIImage(systemName: "arrow.up"), for: .normal)
158
+ sendButton.layer.cornerRadius = CGFloat(Metrics.composerSend) / 2
159
+ sendButton.addTarget(self, action: #selector(sendTapped), for: .touchUpInside)
160
+ sendButton.translatesAutoresizingMaskIntoConstraints = false
161
+
162
+ pill.addSubview(textView)
163
+ pill.addSubview(placeholder)
164
+ pill.addSubview(sendButton)
165
+
166
+ // The send circle is 36 pt. The touch area is 44 pt (the pill is 44 pt high).
167
+ NSLayoutConstraint.activate([
168
+ sendButton.widthAnchor.constraint(equalToConstant: CGFloat(Metrics.composerSend)),
169
+ sendButton.heightAnchor.constraint(equalToConstant: CGFloat(Metrics.composerSend)),
170
+ sendButton.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -4),
171
+ sendButton.bottomAnchor.constraint(equalTo: pill.bottomAnchor, constant: -4),
172
+ textView.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 16),
173
+ textView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -6),
174
+ textView.topAnchor.constraint(equalTo: pill.topAnchor, constant: 3),
175
+ textView.bottomAnchor.constraint(equalTo: pill.bottomAnchor, constant: -3),
176
+ placeholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor),
177
+ placeholder.trailingAnchor.constraint(lessThanOrEqualTo: textView.trailingAnchor),
178
+ placeholder.topAnchor.constraint(equalTo: textView.topAnchor, constant: 7),
179
+ pill.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
180
+ ])
181
+ let heightConstraint = textView.heightAnchor.constraint(equalToConstant: 36)
182
+ heightConstraint.priority = UILayoutPriority(999)
183
+ heightConstraint.isActive = true
184
+ textHeightConstraint = heightConstraint
185
+
186
+ let inputRow = UIStackView(arrangedSubviews: [attachButton, pill])
187
+ self.inputRow = inputRow
188
+ inputRow.axis = .horizontal
189
+ inputRow.alignment = .bottom
190
+ inputRow.spacing = 6
191
+ NSLayoutConstraint.activate([
192
+ attachButton.widthAnchor.constraint(equalToConstant: 44),
193
+ attachButton.heightAnchor.constraint(equalToConstant: 44),
194
+ ])
195
+
196
+ chips.axis = .horizontal
197
+ chips.spacing = 8
198
+ chips.translatesAutoresizingMaskIntoConstraints = false
199
+ chipScroll.addSubview(chips)
200
+ chipScroll.showsHorizontalScrollIndicator = false
201
+ chipScroll.translatesAutoresizingMaskIntoConstraints = false
202
+ NSLayoutConstraint.activate([
203
+ chips.leadingAnchor.constraint(equalTo: chipScroll.contentLayoutGuide.leadingAnchor, constant: 10),
204
+ chips.trailingAnchor.constraint(equalTo: chipScroll.contentLayoutGuide.trailingAnchor, constant: -10),
205
+ chips.topAnchor.constraint(equalTo: chipScroll.contentLayoutGuide.topAnchor),
206
+ chips.bottomAnchor.constraint(equalTo: chipScroll.contentLayoutGuide.bottomAnchor),
207
+ chips.heightAnchor.constraint(equalTo: chipScroll.frameLayoutGuide.heightAnchor),
208
+ ])
209
+ let height = chipScroll.heightAnchor.constraint(equalToConstant: 0)
210
+ height.isActive = true
211
+ chipHeight = height
212
+
213
+ statusLabel.numberOfLines = 0
214
+ statusLabel.isHidden = true
215
+
216
+ column.axis = .vertical
217
+ column.spacing = 6
218
+ column.translatesAutoresizingMaskIntoConstraints = false
219
+ column.addArrangedSubview(statusLabel)
220
+ column.addArrangedSubview(chipScroll)
221
+ column.addArrangedSubview(inputRow)
222
+ addSubview(column)
223
+
224
+ NSLayoutConstraint.activate([
225
+ topLine.topAnchor.constraint(equalTo: topAnchor),
226
+ topLine.leadingAnchor.constraint(equalTo: leadingAnchor),
227
+ topLine.trailingAnchor.constraint(equalTo: trailingAnchor),
228
+ topLine.heightAnchor.constraint(equalToConstant: 1),
229
+ column.topAnchor.constraint(equalTo: topAnchor, constant: 9),
230
+ column.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
231
+ column.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
232
+ column.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8),
233
+ ])
234
+ refresh()
235
+ }
236
+
237
+ // MARK: Theme
238
+
239
+ func apply(theme: ChatTheme, canAttach: Bool) {
240
+ self.theme = theme
241
+ self.canAttach = canAttach
242
+ let rtl = theme.isRTL
243
+ let semantic: UISemanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
244
+ let views: [UIView] = [self, column, pill, textView, placeholder, sendButton, chipScroll, chips, statusLabel]
245
+ for view in views { view.semanticContentAttribute = semantic }
246
+ inputRow?.semanticContentAttribute = semantic
247
+ semanticContentAttribute = semantic
248
+ backgroundColor = UIColor(hex: theme.palette.bg)
249
+ topLine.backgroundColor = UIColor(hex: theme.palette.line)
250
+ pill.backgroundColor = UIColor(hex: theme.palette.bgAlt)
251
+ pill.layer.borderColor = UIColor(hex: theme.palette.line).cgColor
252
+ attachButton.tintColor = UIColor(hex: theme.palette.ink2)
253
+ attachButton.isHidden = !canAttach
254
+ attachButton.accessibilityLabel = theme.strings.string("composer.attach")
255
+ textView.font = theme.uiFont(.message)
256
+ textView.textColor = UIColor(hex: theme.palette.ink)
257
+ textView.tintColor = UIColor(hex: theme.palette.primary)
258
+ textView.textAlignment = .natural
259
+ textView.keyboardAppearance = theme.palette.isDark ? .dark : .light
260
+ textView.accessibilityLabel = theme.strings.string("composer.placeholder")
261
+ placeholder.text = theme.strings.string("composer.placeholder")
262
+ placeholder.font = theme.uiFont(.message)
263
+ placeholder.textColor = UIColor(hex: theme.palette.ink2)
264
+ placeholder.adjustsFontForContentSizeCategory = true
265
+ placeholder.textAlignment = .natural
266
+ sendButton.accessibilityLabel = theme.strings.string("composer.send")
267
+ statusLabel.font = theme.uiFont(.secondary, size: 13)
268
+ statusLabel.adjustsFontForContentSizeCategory = true
269
+ statusLabel.textColor = UIColor(hex: theme.palette.bad)
270
+ refresh()
271
+ }
272
+
273
+ // MARK: State
274
+
275
+ private func refresh() {
276
+ let hasText = !(textView.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
277
+ placeholder.isHidden = !(textView.text ?? "").isEmpty
278
+ let enabled = hasText || (hasPendingAttachments && allChipsReady)
279
+ sendButton.isEnabled = enabled
280
+ sendButton.backgroundColor = UIColor(hex: enabled ? theme.palette.primary : theme.palette.disabledSend)
281
+ sendButton.tintColor = UIColor(hex: enabled ? theme.palette.onPrimary : theme.palette.ink2)
282
+ updateHeight()
283
+ }
284
+
285
+ private var allChipsReady = true
286
+ func setChipsReady(_ ready: Bool) { allChipsReady = ready; refresh() }
287
+
288
+ private func updateHeight() {
289
+ // Grows to 6 lines, then scrolls.
290
+ let font = textView.font ?? UIFont.preferredFont(forTextStyle: .body)
291
+ let maxHeight = ceil(font.lineHeight * 6) + 14
292
+ let width = Swift.max(textView.bounds.width, 120)
293
+ let fitting = textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
294
+ textView.isScrollEnabled = fitting > maxHeight
295
+ let target = Swift.min(fitting, maxHeight)
296
+ if abs(target - textHeight) > 0.5 {
297
+ textHeight = target
298
+ textHeightConstraint?.constant = target
299
+ setNeedsLayout()
300
+ invalidateIntrinsicContentSize()
301
+ onHeightChange?()
302
+ }
303
+ }
304
+
305
+ override func layoutSubviews() {
306
+ super.layoutSubviews()
307
+ if bounds.width > 0 { updateHeight() }
308
+ }
309
+
310
+ func setStatus(_ text: String?) {
311
+ statusLabel.text = text
312
+ statusLabel.isHidden = (text ?? "").isEmpty
313
+ invalidateIntrinsicContentSize()
314
+ onHeightChange?()
315
+ }
316
+
317
+ func setChips(_ views: [PendingChip]) {
318
+ for view in chips.arrangedSubviews { chips.removeArrangedSubview(view); view.removeFromSuperview() }
319
+ for view in views { chips.addArrangedSubview(view) }
320
+ hasPendingAttachments = !views.isEmpty
321
+ chipHeight?.constant = views.isEmpty ? 0 : 56
322
+ chipScroll.isHidden = views.isEmpty
323
+ refresh()
324
+ invalidateIntrinsicContentSize()
325
+ onHeightChange?()
326
+ }
327
+
328
+ /// The attach button is off at the limit of files for one message. `canAttach` (from the server) can still hide it.
329
+ func setAttachEnabled(_ enabled: Bool) {
330
+ attachButton.isEnabled = enabled
331
+ attachButton.alpha = enabled ? 1 : 0.4
332
+ }
333
+
334
+ func clear() {
335
+ textView.text = ""
336
+ refresh()
337
+ }
338
+
339
+ // MARK: Actions
340
+
341
+ @objc private func sendTapped() {
342
+ let value = textView.text ?? ""
343
+ onSend?(value)
344
+ }
345
+
346
+ @objc private func attachTapped() { onAttach?() }
347
+
348
+ func textViewDidChange(_ textView: UITextView) {
349
+ refresh()
350
+ onTextChange?(textView.text ?? "")
351
+ }
352
+
353
+ var attachAnchor: UIView { attachButton }
354
+ }
355
+ #endif