@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,155 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// The small banner for a new message while the chat is closed and the app is open.
|
|
5
|
+
/// It hides after 4 s, swipe up closes it, a tap opens the chat.
|
|
6
|
+
final class InAppBannerController {
|
|
7
|
+
private var view: BannerView?
|
|
8
|
+
private var hideTask: DispatchWorkItem?
|
|
9
|
+
|
|
10
|
+
func show(name: String, brand: String, text: String, avatarURL: String?, theme: ChatTheme, in window: UIWindow?, onTap: @escaping () -> Void) {
|
|
11
|
+
guard let window = window else { return }
|
|
12
|
+
hide(animated: false)
|
|
13
|
+
let banner = BannerView(name: name, text: text, avatarURL: avatarURL, theme: theme)
|
|
14
|
+
banner.semanticContentAttribute = theme.isRTL ? .forceRightToLeft : .forceLeftToRight
|
|
15
|
+
banner.onTap = { [weak self] in
|
|
16
|
+
self?.hide(animated: true)
|
|
17
|
+
onTap()
|
|
18
|
+
}
|
|
19
|
+
banner.onSwipeUp = { [weak self] in self?.hide(animated: true) }
|
|
20
|
+
banner.translatesAutoresizingMaskIntoConstraints = false
|
|
21
|
+
window.addSubview(banner)
|
|
22
|
+
let guide = window.safeAreaLayoutGuide
|
|
23
|
+
NSLayoutConstraint.activate([
|
|
24
|
+
banner.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8),
|
|
25
|
+
banner.centerXAnchor.constraint(equalTo: window.centerXAnchor),
|
|
26
|
+
banner.widthAnchor.constraint(lessThanOrEqualToConstant: 520),
|
|
27
|
+
banner.leadingAnchor.constraint(greaterThanOrEqualTo: guide.leadingAnchor, constant: 12),
|
|
28
|
+
banner.trailingAnchor.constraint(lessThanOrEqualTo: guide.trailingAnchor, constant: -12),
|
|
29
|
+
])
|
|
30
|
+
// On a phone the banner fills the width between the margins.
|
|
31
|
+
let wide = banner.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 12)
|
|
32
|
+
wide.priority = .defaultHigh
|
|
33
|
+
wide.isActive = true
|
|
34
|
+
window.layoutIfNeeded()
|
|
35
|
+
view = banner
|
|
36
|
+
let reduced = UIAccessibility.isReduceMotionEnabled
|
|
37
|
+
banner.alpha = 0
|
|
38
|
+
banner.transform = reduced ? .identity : CGAffineTransform(translationX: 0, y: -banner.bounds.height - 20)
|
|
39
|
+
UIView.animate(withDuration: reduced ? 0.01 : 0.28, delay: 0, options: .curveEaseOut) {
|
|
40
|
+
banner.alpha = 1
|
|
41
|
+
banner.transform = .identity
|
|
42
|
+
}
|
|
43
|
+
UIAccessibility.post(notification: .announcement, argument: "\(name). \(text)")
|
|
44
|
+
let work = DispatchWorkItem { [weak self] in self?.hide(animated: true) }
|
|
45
|
+
hideTask = work
|
|
46
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: work)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func hide(animated: Bool) {
|
|
50
|
+
hideTask?.cancel()
|
|
51
|
+
hideTask = nil
|
|
52
|
+
guard let banner = view else { return }
|
|
53
|
+
view = nil
|
|
54
|
+
guard animated else {
|
|
55
|
+
banner.removeFromSuperview()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
let reduced = UIAccessibility.isReduceMotionEnabled
|
|
59
|
+
UIView.animate(withDuration: reduced ? 0.01 : 0.22, animations: {
|
|
60
|
+
banner.alpha = 0
|
|
61
|
+
if !reduced { banner.transform = CGAffineTransform(translationX: 0, y: -banner.bounds.height - 20) }
|
|
62
|
+
}, completion: { _ in banner.removeFromSuperview() })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private final class BannerView: UIView {
|
|
67
|
+
var onTap: () -> Void = {}
|
|
68
|
+
var onSwipeUp: () -> Void = {}
|
|
69
|
+
|
|
70
|
+
init(name: String, text: String, avatarURL: String?, theme: ChatTheme) {
|
|
71
|
+
super.init(frame: .zero)
|
|
72
|
+
let palette = theme.palette
|
|
73
|
+
backgroundColor = theme.uiColor(palette.bg)
|
|
74
|
+
layer.cornerRadius = 16
|
|
75
|
+
layer.shadowColor = UIColor.black.cgColor
|
|
76
|
+
layer.shadowOpacity = 0.18
|
|
77
|
+
layer.shadowRadius = 14
|
|
78
|
+
layer.shadowOffset = CGSize(width: 0, height: 4)
|
|
79
|
+
layer.borderWidth = 1
|
|
80
|
+
layer.borderColor = theme.uiColor(palette.line).cgColor
|
|
81
|
+
|
|
82
|
+
let avatar = UIImageView()
|
|
83
|
+
avatar.backgroundColor = theme.uiColor(palette.avatarTone)
|
|
84
|
+
avatar.layer.cornerRadius = 18
|
|
85
|
+
avatar.clipsToBounds = true
|
|
86
|
+
avatar.contentMode = .scaleAspectFill
|
|
87
|
+
avatar.translatesAutoresizingMaskIntoConstraints = false
|
|
88
|
+
avatar.isAccessibilityElement = false
|
|
89
|
+
let initial = UILabel()
|
|
90
|
+
initial.text = String(name.prefix(1)).uppercased()
|
|
91
|
+
initial.font = theme.uiFont(.secondary, size: 15, weight: .semibold)
|
|
92
|
+
initial.textColor = theme.uiColor(palette.avatarInk)
|
|
93
|
+
initial.translatesAutoresizingMaskIntoConstraints = false
|
|
94
|
+
avatar.addSubview(initial)
|
|
95
|
+
NSLayoutConstraint.activate([
|
|
96
|
+
initial.centerXAnchor.constraint(equalTo: avatar.centerXAnchor),
|
|
97
|
+
initial.centerYAnchor.constraint(equalTo: avatar.centerYAnchor),
|
|
98
|
+
avatar.widthAnchor.constraint(equalToConstant: 36),
|
|
99
|
+
avatar.heightAnchor.constraint(equalToConstant: 36),
|
|
100
|
+
])
|
|
101
|
+
if let urlString = avatarURL {
|
|
102
|
+
Task { [weak avatar, weak initial] in
|
|
103
|
+
if let image = try? await ImageLoader.shared.load(urlString) {
|
|
104
|
+
await MainActor.run {
|
|
105
|
+
avatar?.image = image
|
|
106
|
+
initial?.isHidden = true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let title = UILabel()
|
|
113
|
+
title.text = name
|
|
114
|
+
title.font = theme.uiFont(.secondary, size: 14, weight: .semibold)
|
|
115
|
+
title.textColor = theme.uiColor(palette.ink)
|
|
116
|
+
title.adjustsFontForContentSizeCategory = true
|
|
117
|
+
let body = UILabel()
|
|
118
|
+
body.text = text
|
|
119
|
+
body.font = theme.uiFont(.secondary, size: 13.5)
|
|
120
|
+
body.textColor = theme.uiColor(palette.ink2)
|
|
121
|
+
body.numberOfLines = 2
|
|
122
|
+
body.adjustsFontForContentSizeCategory = true
|
|
123
|
+
let stack = UIStackView(arrangedSubviews: [title, body])
|
|
124
|
+
stack.axis = .vertical
|
|
125
|
+
stack.spacing = 2
|
|
126
|
+
let row = UIStackView(arrangedSubviews: [avatar, stack])
|
|
127
|
+
row.spacing = 12
|
|
128
|
+
row.alignment = .center
|
|
129
|
+
row.translatesAutoresizingMaskIntoConstraints = false
|
|
130
|
+
addSubview(row)
|
|
131
|
+
NSLayoutConstraint.activate([
|
|
132
|
+
row.topAnchor.constraint(equalTo: topAnchor, constant: 12),
|
|
133
|
+
row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
|
|
134
|
+
row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
|
|
135
|
+
row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
|
|
136
|
+
])
|
|
137
|
+
isAccessibilityElement = true
|
|
138
|
+
accessibilityLabel = "\(name). \(text)"
|
|
139
|
+
accessibilityTraits = .button
|
|
140
|
+
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
|
|
141
|
+
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swiped))
|
|
142
|
+
swipe.direction = .up
|
|
143
|
+
addGestureRecognizer(swipe)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
required init?(coder: NSCoder) { return nil }
|
|
147
|
+
|
|
148
|
+
@objc private func tapped() { onTap() }
|
|
149
|
+
@objc private func swiped() { onSwipeUp() }
|
|
150
|
+
override func accessibilityActivate() -> Bool {
|
|
151
|
+
onTap()
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
#endif
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && canImport(UIKit)
|
|
2
|
+
import Combine
|
|
3
|
+
import Foundation
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
public enum ZellioChatLogLevel: Int, Sendable {
|
|
7
|
+
case none = 0
|
|
8
|
+
case error = 1
|
|
9
|
+
case info = 2
|
|
10
|
+
case debug = 3
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public enum ZellioChatLocale: Sendable {
|
|
14
|
+
/// Follow the device, then the Studio language policy.
|
|
15
|
+
case auto
|
|
16
|
+
case arabic
|
|
17
|
+
case english
|
|
18
|
+
|
|
19
|
+
var core: ChatLocale? {
|
|
20
|
+
switch self {
|
|
21
|
+
case .auto: return nil
|
|
22
|
+
case .arabic: return .ar
|
|
23
|
+
case .english: return .en
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public enum ZellioChatThemeMode: Sendable {
|
|
29
|
+
case auto
|
|
30
|
+
case light
|
|
31
|
+
case dark
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Overrides for the look. A value set here wins over Studio.
|
|
35
|
+
public struct ZellioChatTheme {
|
|
36
|
+
public var mode: ZellioChatThemeMode
|
|
37
|
+
public var primaryColor: UIColor?
|
|
38
|
+
public var customerMessageColor: UIColor?
|
|
39
|
+
public var surfaceColor: UIColor?
|
|
40
|
+
/// The PostScript name of a font that the app has registered. Sizes still follow Dynamic Type.
|
|
41
|
+
public var fontName: String?
|
|
42
|
+
|
|
43
|
+
public init(mode: ZellioChatThemeMode = .auto, primaryColor: UIColor? = nil, customerMessageColor: UIColor? = nil,
|
|
44
|
+
surfaceColor: UIColor? = nil, fontName: String? = nil) {
|
|
45
|
+
self.mode = mode
|
|
46
|
+
self.primaryColor = primaryColor
|
|
47
|
+
self.customerMessageColor = customerMessageColor
|
|
48
|
+
self.surfaceColor = surfaceColor
|
|
49
|
+
self.fontName = fontName
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var coreMode: ThemeMode? {
|
|
53
|
+
switch mode {
|
|
54
|
+
case .auto: return nil
|
|
55
|
+
case .light: return .light
|
|
56
|
+
case .dark: return .dark
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public struct ZellioChatOptions {
|
|
62
|
+
/// Default: https://app.zellio.ai
|
|
63
|
+
public var baseURL: URL?
|
|
64
|
+
public var locale: ZellioChatLocale
|
|
65
|
+
public var theme: ZellioChatTheme
|
|
66
|
+
public var logLevel: ZellioChatLogLevel
|
|
67
|
+
/// The small banner for a new message while chat is closed and the app is open.
|
|
68
|
+
public var showsInAppBanner: Bool
|
|
69
|
+
/// false: the banner says "New message" and never shows the text.
|
|
70
|
+
public var bannerShowsPreview: Bool
|
|
71
|
+
/// Set by the React Native and Flutter wrappers: "react-native/1.0.0".
|
|
72
|
+
public var wrapper: String?
|
|
73
|
+
/// For demos and tests only: replaces the network. Leave nil in a real app.
|
|
74
|
+
public var transport: HTTPTransport?
|
|
75
|
+
|
|
76
|
+
public init(baseURL: URL? = nil, locale: ZellioChatLocale = .auto, theme: ZellioChatTheme = ZellioChatTheme(),
|
|
77
|
+
logLevel: ZellioChatLogLevel = .error, showsInAppBanner: Bool = true, bannerShowsPreview: Bool = true, wrapper: String? = nil, transport: HTTPTransport? = nil) {
|
|
78
|
+
self.baseURL = baseURL
|
|
79
|
+
self.locale = locale
|
|
80
|
+
self.theme = theme
|
|
81
|
+
self.logLevel = logLevel
|
|
82
|
+
self.showsInAppBanner = showsInAppBanner
|
|
83
|
+
self.bannerShowsPreview = bannerShowsPreview
|
|
84
|
+
self.transport = transport
|
|
85
|
+
self.wrapper = wrapper
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// The page or item the customer looks at. Sent with the next messages. Agents see it.
|
|
90
|
+
public struct ZellioChatContext: Equatable, Sendable {
|
|
91
|
+
public var screen: String?
|
|
92
|
+
public var itemType: String?
|
|
93
|
+
public var itemId: String?
|
|
94
|
+
public var itemTitle: String?
|
|
95
|
+
/// Must be https. Other values are dropped.
|
|
96
|
+
public var url: String?
|
|
97
|
+
|
|
98
|
+
public init(screen: String? = nil, itemType: String? = nil, itemId: String? = nil, itemTitle: String? = nil, url: String? = nil) {
|
|
99
|
+
self.screen = screen
|
|
100
|
+
self.itemType = itemType
|
|
101
|
+
self.itemId = itemId
|
|
102
|
+
self.itemTitle = itemTitle
|
|
103
|
+
self.url = url
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var core: ChatContext { ChatContext(screen: screen, itemType: itemType, itemId: itemId, itemTitle: itemTitle, url: url) }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Events for the brand's own analytics. They never carry message text.
|
|
110
|
+
public enum ZellioChatEvent: Equatable, Sendable {
|
|
111
|
+
case chatOpened
|
|
112
|
+
case chatClosed
|
|
113
|
+
case messageSent
|
|
114
|
+
case messageReceived
|
|
115
|
+
case error(code: String)
|
|
116
|
+
|
|
117
|
+
public var name: String {
|
|
118
|
+
switch self {
|
|
119
|
+
case .chatOpened: return "chatOpened"
|
|
120
|
+
case .chatClosed: return "chatClosed"
|
|
121
|
+
case .messageSent: return "messageSent"
|
|
122
|
+
case .messageReceived: return "messageReceived"
|
|
123
|
+
case .error: return "error"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public var code: String? {
|
|
128
|
+
if case .error(let code) = self { return code }
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Async source of the brand's identity JWT. Called when the SDK needs a session.
|
|
134
|
+
public typealias ZellioChatTokenProvider = @Sendable () async throws -> String
|
|
135
|
+
|
|
136
|
+
/// The unread number, for SwiftUI and UIKit.
|
|
137
|
+
public final class ZellioChatUnread: ObservableObject {
|
|
138
|
+
@Published public internal(set) var count: Int = 0
|
|
139
|
+
init() {}
|
|
140
|
+
}
|
|
141
|
+
#endif
|