@capgo/capacitor-stream-call 0.0.18 → 0.0.20
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/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +41 -4
- package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +10 -70
- package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +0 -26
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +256 -268
- package/package.json +1 -1
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +0 -177
|
@@ -29,7 +29,6 @@ import io.getstream.video.android.core.StreamVideo
|
|
|
29
29
|
import io.getstream.video.android.core.StreamVideoBuilder
|
|
30
30
|
import io.getstream.video.android.core.notifications.NotificationConfig
|
|
31
31
|
import io.getstream.video.android.core.notifications.NotificationHandler
|
|
32
|
-
import io.getstream.video.android.core.sounds.emptyRingingConfig
|
|
33
32
|
import io.getstream.video.android.core.sounds.toSounds
|
|
34
33
|
import io.getstream.video.android.model.StreamCallId
|
|
35
34
|
import io.getstream.video.android.model.User
|
|
@@ -46,6 +45,7 @@ import io.getstream.android.video.generated.models.CallCreatedEvent
|
|
|
46
45
|
import io.getstream.android.video.generated.models.CallEndedEvent
|
|
47
46
|
import io.getstream.android.video.generated.models.CallMissedEvent
|
|
48
47
|
import io.getstream.android.video.generated.models.CallRejectedEvent
|
|
48
|
+
import io.getstream.android.video.generated.models.CallRingEvent
|
|
49
49
|
import io.getstream.android.video.generated.models.CallSessionEndedEvent
|
|
50
50
|
import io.getstream.android.video.generated.models.CallSessionStartedEvent
|
|
51
51
|
import io.getstream.android.video.generated.models.VideoEvent
|
|
@@ -185,7 +185,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
185
185
|
declineCall(declinedCall)
|
|
186
186
|
},
|
|
187
187
|
onAcceptCall = { acceptedCall ->
|
|
188
|
-
|
|
188
|
+
internalAcceptCall(acceptedCall)
|
|
189
189
|
},
|
|
190
190
|
onHideIncomingCall = {
|
|
191
191
|
hideIncomingCall()
|
|
@@ -207,7 +207,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
207
207
|
val call = streamVideoClient?.call(id = cid.id, type = cid.type)
|
|
208
208
|
kotlinx.coroutines.GlobalScope.launch {
|
|
209
209
|
call?.get()
|
|
210
|
-
call?.let {
|
|
210
|
+
call?.let { internalAcceptCall(it) }
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -565,6 +565,9 @@ public class StreamCallPlugin : Plugin() {
|
|
|
565
565
|
client.subscribe { event: VideoEvent ->
|
|
566
566
|
android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
|
|
567
567
|
when (event) {
|
|
568
|
+
is CallRingEvent -> {
|
|
569
|
+
updateCallStatusAndNotify(event.callCid, "ringing")
|
|
570
|
+
}
|
|
568
571
|
// Handle CallCreatedEvent differently - only log it but don't try to access members yet
|
|
569
572
|
is CallCreatedEvent -> {
|
|
570
573
|
val callCid = event.callCid
|
|
@@ -744,8 +747,42 @@ public class StreamCallPlugin : Plugin() {
|
|
|
744
747
|
})
|
|
745
748
|
}
|
|
746
749
|
|
|
750
|
+
@PluginMethod
|
|
751
|
+
public fun acceptCall(call: PluginCall) {
|
|
752
|
+
try {
|
|
753
|
+
val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
|
|
754
|
+
if (streamVideoCall == null) {
|
|
755
|
+
call.reject("Ringing call is null")
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
759
|
+
internalAcceptCall(streamVideoCall)
|
|
760
|
+
}
|
|
761
|
+
} catch (t: Throwable) {
|
|
762
|
+
android.util.Log.d("StreamCallPlugin", "JS -> acceptCall fail", t);
|
|
763
|
+
call.reject("Cannot acceptCall")
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
@PluginMethod
|
|
768
|
+
public fun rejectCall(call: PluginCall) {
|
|
769
|
+
try {
|
|
770
|
+
val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
|
|
771
|
+
if (streamVideoCall == null) {
|
|
772
|
+
call.reject("Ringing call is null")
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
776
|
+
declineCall(streamVideoCall)
|
|
777
|
+
}
|
|
778
|
+
} catch (t: Throwable) {
|
|
779
|
+
android.util.Log.d("StreamCallPlugin", "JS -> rejectCall fail", t);
|
|
780
|
+
call.reject("Cannot rejectCall")
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
747
784
|
@OptIn(DelicateCoroutinesApi::class)
|
|
748
|
-
private fun
|
|
785
|
+
private fun internalAcceptCall(call: Call) {
|
|
749
786
|
kotlinx.coroutines.GlobalScope.launch {
|
|
750
787
|
try {
|
|
751
788
|
// Stop ringtone
|
|
@@ -3,53 +3,6 @@ import StreamVideo
|
|
|
3
3
|
import StreamVideoSwiftUI
|
|
4
4
|
import Combine
|
|
5
5
|
|
|
6
|
-
class CallOverlayViewModel: ObservableObject {
|
|
7
|
-
@Published var streamVideo: StreamVideo?
|
|
8
|
-
@Published var call: Call?
|
|
9
|
-
@Published var callState: CallState?
|
|
10
|
-
@Published var viewModel: CallViewModel?
|
|
11
|
-
@Published var participants: [CallParticipant] = []
|
|
12
|
-
|
|
13
|
-
private var participantsSubscription: AnyCancellable?
|
|
14
|
-
|
|
15
|
-
init(streamVideo: StreamVideo?) {
|
|
16
|
-
self.streamVideo = streamVideo
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
@MainActor
|
|
20
|
-
func updateCall(_ call: Call?) {
|
|
21
|
-
self.call = call
|
|
22
|
-
// Clean up previous subscription if any
|
|
23
|
-
participantsSubscription?.cancel()
|
|
24
|
-
|
|
25
|
-
if let call = call {
|
|
26
|
-
participantsSubscription = call.state.$participants.sink { [weak self] participants in
|
|
27
|
-
print("Participants update \(participants.map { $0.name })")
|
|
28
|
-
self?.participants = participants
|
|
29
|
-
}
|
|
30
|
-
self.callState = call.state
|
|
31
|
-
participantsSubscription = call.state.$callSettings.sink { [weak self] callSettings in
|
|
32
|
-
print("Call settings update")
|
|
33
|
-
self?.viewModel = CallViewModel(callSettings: callSettings)
|
|
34
|
-
self?.viewModel?.setActiveCall(call)
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
// Clear participants when call ends
|
|
38
|
-
self.participants = []
|
|
39
|
-
self.callState = nil
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@MainActor
|
|
44
|
-
func updateStreamVideo(_ streamVideo: StreamVideo?) {
|
|
45
|
-
self.streamVideo = streamVideo
|
|
46
|
-
if streamVideo == nil {
|
|
47
|
-
self.call = nil
|
|
48
|
-
self.callState = nil
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
6
|
class CallOverlayViewFactory: ViewFactory {
|
|
54
7
|
// ... existing ViewFactory methods ...
|
|
55
8
|
func makeVideoParticipantView(
|
|
@@ -73,26 +26,22 @@ class CallOverlayViewFactory: ViewFactory {
|
|
|
73
26
|
}
|
|
74
27
|
|
|
75
28
|
struct CallOverlayView: View {
|
|
76
|
-
@ObservedObject var viewModel:
|
|
29
|
+
@ObservedObject var viewModel: CallViewModel
|
|
77
30
|
@State private var safeAreaInsets: EdgeInsets = .init()
|
|
78
31
|
private let viewFactory: CallOverlayViewFactory
|
|
79
32
|
|
|
80
|
-
init(viewModel:
|
|
33
|
+
init(viewModel: CallViewModel) {
|
|
81
34
|
self.viewModel = viewModel
|
|
82
35
|
self.viewFactory = CallOverlayViewFactory()
|
|
83
36
|
}
|
|
84
37
|
|
|
85
38
|
var body: some View {
|
|
86
39
|
VStack(spacing: 0) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
CustomCallView(viewFactory: viewFactory, viewModel: viewModelStandard)
|
|
90
|
-
}
|
|
91
|
-
.padding(.top, safeAreaInsets.top)
|
|
92
|
-
.padding(.bottom, safeAreaInsets.bottom)
|
|
93
|
-
} else {
|
|
94
|
-
Color.white
|
|
40
|
+
ZStack {
|
|
41
|
+
CustomCallView(viewFactory: viewFactory, viewModel: viewModel)
|
|
95
42
|
}
|
|
43
|
+
.padding(.top, safeAreaInsets.top)
|
|
44
|
+
.padding(.bottom, safeAreaInsets.bottom)
|
|
96
45
|
}
|
|
97
46
|
.edgesIgnoringSafeArea(.all)
|
|
98
47
|
.overlay(
|
|
@@ -108,7 +57,7 @@ struct CallOverlayView: View {
|
|
|
108
57
|
}
|
|
109
58
|
|
|
110
59
|
private func changeTrackVisibility(_ participant: CallParticipant?, isVisible: Bool) {
|
|
111
|
-
print("changeTrackVisibility for \(participant?.userId), visible: \(isVisible)")
|
|
60
|
+
print("changeTrackVisibility for \(String(describing: participant?.userId)), visible: \(isVisible)")
|
|
112
61
|
guard let participant = participant,
|
|
113
62
|
let call = viewModel.call else { return }
|
|
114
63
|
Task {
|
|
@@ -118,26 +67,17 @@ struct CallOverlayView: View {
|
|
|
118
67
|
}
|
|
119
68
|
|
|
120
69
|
extension CallOverlayView {
|
|
121
|
-
static func create(
|
|
122
|
-
let
|
|
123
|
-
let view = CallOverlayView(viewModel: viewModel)
|
|
70
|
+
static func create(callViewModel: CallViewModel) -> UIHostingController<CallOverlayView> {
|
|
71
|
+
let view = CallOverlayView(viewModel: callViewModel)
|
|
124
72
|
let hostingController = UIHostingController(rootView: view)
|
|
125
73
|
hostingController.view.backgroundColor = .clear
|
|
126
74
|
|
|
127
75
|
// Make sure we respect safe areas
|
|
128
76
|
hostingController.view.insetsLayoutMarginsFromSafeArea = true
|
|
129
77
|
|
|
130
|
-
return (hostingController
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
#if DEBUG
|
|
135
|
-
struct CallOverlayView_Previews: PreviewProvider {
|
|
136
|
-
static var previews: some View {
|
|
137
|
-
CallOverlayView(viewModel: CallOverlayViewModel(streamVideo: nil))
|
|
78
|
+
return (hostingController)
|
|
138
79
|
}
|
|
139
80
|
}
|
|
140
|
-
#endif
|
|
141
81
|
|
|
142
82
|
struct SafeAreaInsetsKey: PreferenceKey {
|
|
143
83
|
static var defaultValue: EdgeInsets = .init()
|
|
@@ -50,18 +50,6 @@ struct ParticipantsView: View {
|
|
|
50
50
|
var localParticipant: CallParticipant
|
|
51
51
|
@State private var labeledFrames: [ViewFramePreferenceData] = []
|
|
52
52
|
|
|
53
|
-
private func findTouchInterceptView() -> TouchInterceptView? {
|
|
54
|
-
// Find the TouchInterceptView by traversing up the view hierarchy
|
|
55
|
-
var currentView = UIApplication.shared.windows.first?.rootViewController?.view
|
|
56
|
-
while let view = currentView {
|
|
57
|
-
if let touchInterceptView = view as? TouchInterceptView {
|
|
58
|
-
return touchInterceptView
|
|
59
|
-
}
|
|
60
|
-
currentView = view.superview
|
|
61
|
-
}
|
|
62
|
-
return nil
|
|
63
|
-
}
|
|
64
|
-
|
|
65
53
|
var body: some View {
|
|
66
54
|
GeometryReader { proxy in
|
|
67
55
|
if !participants.isEmpty {
|
|
@@ -170,20 +158,6 @@ struct ParticipantsView: View {
|
|
|
170
158
|
}
|
|
171
159
|
}
|
|
172
160
|
}
|
|
173
|
-
.onPreferenceChange(ViewFramePreferenceKey.self) { frames in
|
|
174
|
-
print("ParticipantsView - Received frame updates:")
|
|
175
|
-
print("Number of frames: \(frames.count)")
|
|
176
|
-
frames.forEach { frame in
|
|
177
|
-
print("Label: \(frame.label), Frame: \(frame.frame)")
|
|
178
|
-
}
|
|
179
|
-
self.labeledFrames = frames
|
|
180
|
-
if let touchInterceptView = findTouchInterceptView() {
|
|
181
|
-
print("ParticipantsView - Found TouchInterceptView, updating frames")
|
|
182
|
-
touchInterceptView.updateLabeledFrames(frames)
|
|
183
|
-
} else {
|
|
184
|
-
print("ParticipantsView - Failed to find TouchInterceptView!")
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
161
|
} else {
|
|
188
162
|
Color.gray
|
|
189
163
|
}
|
|
@@ -41,20 +41,16 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
41
41
|
|
|
42
42
|
private var overlayView: UIView?
|
|
43
43
|
private var hostingController: UIHostingController<CallOverlayView>?
|
|
44
|
-
private var overlayViewModel: CallOverlayViewModel?
|
|
45
44
|
private var tokenSubscription: AnyCancellable?
|
|
46
45
|
private var activeCallSubscription: AnyCancellable?
|
|
47
46
|
private var lastVoIPToken: String?
|
|
48
|
-
private var touchInterceptView: TouchInterceptView?
|
|
49
47
|
|
|
50
48
|
private var streamVideo: StreamVideo?
|
|
51
49
|
|
|
52
|
-
// Track the current active call ID
|
|
53
|
-
private var currentActiveCallId: String?
|
|
54
|
-
|
|
55
50
|
// Store current call info for getCallStatus
|
|
56
51
|
private var currentCallId: String = ""
|
|
57
52
|
private var currentCallState: String = ""
|
|
53
|
+
private var hasNotifiedCallJoined: Bool = false
|
|
58
54
|
|
|
59
55
|
@Injected(\.callKitAdapter) var callKitAdapter
|
|
60
56
|
@Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
|
|
@@ -63,6 +59,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
63
59
|
// Add class property to store call states
|
|
64
60
|
private var callStates: [String: (members: [MemberResponse], participantResponses: [String: String], createdAt: Date, timer: Timer?)] = [:]
|
|
65
61
|
|
|
62
|
+
// Declare as optional and initialize in load() method
|
|
63
|
+
private var callViewModel: CallViewModel?
|
|
64
|
+
|
|
66
65
|
// Helper method to update call status and notify listeners
|
|
67
66
|
private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil) {
|
|
68
67
|
// Update stored call info
|
|
@@ -95,7 +94,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
95
94
|
if self.apiKey == nil {
|
|
96
95
|
fatalError("Cannot get apikey")
|
|
97
96
|
}
|
|
98
|
-
|
|
97
|
+
|
|
99
98
|
// Check if we have a logged in user for handling incoming calls
|
|
100
99
|
if let credentials = SecureUserRepository.shared.loadCurrentUser() {
|
|
101
100
|
print("Loading user for StreamCallPlugin: \(credentials.user.name)")
|
|
@@ -118,32 +117,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
118
117
|
self.webView?.navigationDelegate = self.webviewDelegate
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
// private func cleanupStreamVideo() {
|
|
122
|
-
// // Cancel subscriptions
|
|
123
|
-
// tokenSubscription?.cancel()
|
|
124
|
-
// tokenSubscription = nil
|
|
125
|
-
// activeCallSubscription?.cancel()
|
|
126
|
-
// activeCallSubscription = nil
|
|
127
|
-
// lastVoIPToken = nil
|
|
128
|
-
//
|
|
129
|
-
// // Cleanup UI
|
|
130
|
-
// Task { @MainActor in
|
|
131
|
-
// self.overlayViewModel?.updateCall(nil)
|
|
132
|
-
// self.overlayViewModel?.updateStreamVideo(nil)
|
|
133
|
-
// self.overlayView?.removeFromSuperview()
|
|
134
|
-
// self.overlayView = nil
|
|
135
|
-
// self.hostingController = nil
|
|
136
|
-
// self.overlayViewModel = nil
|
|
137
|
-
//
|
|
138
|
-
// // Reset webview
|
|
139
|
-
// self.webView?.isOpaque = true
|
|
140
|
-
// self.webView?.backgroundColor = .white
|
|
141
|
-
// self.webView?.scrollView.backgroundColor = .white
|
|
142
|
-
// }
|
|
143
|
-
//
|
|
144
|
-
// state = .notInitialized
|
|
145
|
-
// }
|
|
146
|
-
|
|
147
120
|
private func requireInitialized() throws {
|
|
148
121
|
guard state == .initialized else {
|
|
149
122
|
throw NSError(domain: "StreamCallPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "StreamVideo not initialized"])
|
|
@@ -192,191 +165,132 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
192
165
|
}
|
|
193
166
|
|
|
194
167
|
private func setupActiveCallSubscription() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if let rejectedEvent = event.rawValue as? CallRejectedEvent {
|
|
232
|
-
let userId = rejectedEvent.user.id
|
|
233
|
-
let callCid = rejectedEvent.callCid
|
|
234
|
-
|
|
235
|
-
// Operate on callStates on the main thread
|
|
236
|
-
await MainActor.run {
|
|
237
|
-
// Update the combined callStates map
|
|
238
|
-
if var callState = self.callStates[callCid] {
|
|
239
|
-
callState.participantResponses[userId] = "rejected"
|
|
240
|
-
self.callStates[callCid] = callState
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
print("CallRejectedEvent \(userId)")
|
|
245
|
-
updateCallStatusAndNotify(callId: callCid, state: "rejected", userId: userId)
|
|
246
|
-
|
|
247
|
-
await checkAllParticipantsResponded(callCid: callCid)
|
|
248
|
-
continue
|
|
168
|
+
// Ensure this method is called on the main thread and properly establishes the subscription
|
|
169
|
+
DispatchQueue.main.async { [weak self] in
|
|
170
|
+
guard let self = self else { return }
|
|
171
|
+
|
|
172
|
+
// Cancel existing subscription if any
|
|
173
|
+
self.activeCallSubscription?.cancel()
|
|
174
|
+
self.activeCallSubscription = nil
|
|
175
|
+
|
|
176
|
+
// Verify callViewModel exists
|
|
177
|
+
guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
|
|
178
|
+
print("Warning: setupActiveCallSubscription called but callViewModel or streamVideo is nil")
|
|
179
|
+
// Schedule a retry after a short delay if callViewModel is nil
|
|
180
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
|
181
|
+
self?.setupActiveCallSubscription()
|
|
182
|
+
}
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
print("Setting up active call subscription")
|
|
187
|
+
|
|
188
|
+
// Create a strong reference to callViewModel to ensure it's not deallocated
|
|
189
|
+
// while the subscription is active
|
|
190
|
+
let viewModel = callViewModel
|
|
191
|
+
|
|
192
|
+
// Subscribe to streamVideo.state.$activeCall to handle CallKit integration
|
|
193
|
+
let callPublisher = streamVideo.state.$activeCall
|
|
194
|
+
.receive(on: DispatchQueue.main)
|
|
195
|
+
.sink { [weak self, weak viewModel] activeCall in
|
|
196
|
+
guard let self = self, let viewModel = viewModel else { return }
|
|
197
|
+
|
|
198
|
+
print("Active call update from streamVideo: \(String(describing: activeCall?.cId))")
|
|
199
|
+
|
|
200
|
+
if let activeCall = activeCall {
|
|
201
|
+
// Sync callViewModel with activeCall from streamVideo state
|
|
202
|
+
// This ensures CallKit integration works properly
|
|
203
|
+
viewModel.setActiveCall(activeCall)
|
|
249
204
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
print("CallMissedEvent \(userId)")
|
|
265
|
-
updateCallStatusAndNotify(callId: callCid, state: "missed", userId: userId)
|
|
266
|
-
|
|
267
|
-
await checkAllParticipantsResponded(callCid: callCid)
|
|
268
|
-
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Store the subscription for activeCall updates
|
|
208
|
+
self.activeCallSubscription = callPublisher
|
|
209
|
+
|
|
210
|
+
// Additionally, subscribe to callingState for other call state changes
|
|
211
|
+
let statePublisher = viewModel.$callingState
|
|
212
|
+
.receive(on: DispatchQueue.main)
|
|
213
|
+
.sink { [weak self, weak viewModel] newState in
|
|
214
|
+
guard let self = self, let viewModel = viewModel else {
|
|
215
|
+
print("Warning: Call state update received but self or viewModel is nil")
|
|
216
|
+
return
|
|
269
217
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if let activeCall = streamVideo.state.activeCall {
|
|
288
|
-
activeCall.leave()
|
|
289
|
-
}
|
|
218
|
+
|
|
219
|
+
do {
|
|
220
|
+
try self.requireInitialized()
|
|
221
|
+
print("Call State Update: \(newState)")
|
|
222
|
+
|
|
223
|
+
if newState == .inCall {
|
|
224
|
+
print("- In call state detected")
|
|
225
|
+
print("- All participants: \(String(describing: viewModel.participants))")
|
|
226
|
+
|
|
227
|
+
// Create/update overlay and make visible when there's an active call
|
|
228
|
+
self.createCallOverlayView()
|
|
229
|
+
|
|
230
|
+
// Notify that a call has started - but only if we haven't notified for this call yet
|
|
231
|
+
if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
|
|
232
|
+
print("Notifying call joined: \(callId)")
|
|
233
|
+
self.updateCallStatusAndNotify(callId: callId, state: "joined")
|
|
234
|
+
self.hasNotifiedCallJoined = true
|
|
290
235
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
236
|
+
} else if case .incoming(let incomingCall) = newState {
|
|
237
|
+
self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
|
|
238
|
+
} else if newState == .idle && self.streamVideo?.state.activeCall == nil {
|
|
239
|
+
// Get the call ID that was active before the state changed
|
|
240
|
+
let endingCallId = viewModel.call?.cId
|
|
241
|
+
print("Call ending: \(String(describing: endingCallId))")
|
|
242
|
+
|
|
243
|
+
// Notify that call has ended - use the properly tracked call ID
|
|
244
|
+
self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
|
|
245
|
+
|
|
246
|
+
// Reset notification flag when call ends
|
|
247
|
+
self.hasNotifiedCallJoined = false
|
|
248
|
+
|
|
249
|
+
// Clean up any resources for this call
|
|
250
|
+
if let callCid = endingCallId {
|
|
251
|
+
// Invalidate and remove the timer
|
|
252
|
+
self.callStates[callCid]?.timer?.invalidate()
|
|
253
|
+
|
|
254
|
+
// Remove call from callStates
|
|
255
|
+
self.callStates.removeValue(forKey: callCid)
|
|
256
|
+
|
|
257
|
+
print("Cleaned up resources for ended call: \(callCid)")
|
|
309
258
|
}
|
|
259
|
+
|
|
260
|
+
// Remove the call overlay view when not in a call
|
|
261
|
+
self.ensureViewRemoved()
|
|
310
262
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
updateCallStatusAndNotify(callId: callCid, state: "accepted", userId: userId)
|
|
314
|
-
continue
|
|
263
|
+
} catch {
|
|
264
|
+
log.error("Error handling call state update: \(String(describing: error))")
|
|
315
265
|
}
|
|
316
|
-
|
|
317
|
-
updateCallStatusAndNotify(callId: streamVideo.state.activeCall?.callId ?? "", state: event.type)
|
|
318
266
|
}
|
|
267
|
+
|
|
268
|
+
// Combine both publishers
|
|
269
|
+
self.activeCallSubscription = AnyCancellable {
|
|
270
|
+
callPublisher.cancel()
|
|
271
|
+
statePublisher.cancel()
|
|
319
272
|
}
|
|
273
|
+
|
|
274
|
+
print("Active call subscription setup completed")
|
|
275
|
+
|
|
276
|
+
// Schedule a periodic check to ensure subscription is active
|
|
277
|
+
self.scheduleSubscriptionCheck()
|
|
320
278
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Add a new method to periodically check and restore the subscription if needed
|
|
282
|
+
private func scheduleSubscriptionCheck() {
|
|
283
|
+
// Create a timer that checks the subscription every 5 seconds
|
|
284
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
|
|
325
285
|
guard let self = self else { return }
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
print("- state: \(state)")
|
|
335
|
-
print("- Session ID: \(state.sessionId)")
|
|
336
|
-
print("- All participants: \(String(describing: state.participants))")
|
|
337
|
-
print("- Remote participants: \(String(describing: state.remoteParticipants))")
|
|
338
|
-
|
|
339
|
-
// Store the active call ID when a call becomes active
|
|
340
|
-
self.currentActiveCallId = newState?.cId
|
|
341
|
-
print("Updated current active call ID: \(String(describing: self.currentActiveCallId))")
|
|
342
|
-
|
|
343
|
-
// Update overlay and make visible when there's an active call
|
|
344
|
-
self.overlayViewModel?.updateCall(newState)
|
|
345
|
-
self.overlayView?.isHidden = false
|
|
346
|
-
self.webView?.isOpaque = false
|
|
347
|
-
|
|
348
|
-
// Notify that a call has started
|
|
349
|
-
self.updateCallStatusAndNotify(callId: newState?.cId ?? "", state: "joined")
|
|
350
|
-
} else {
|
|
351
|
-
// Get the call ID that was active before the state changed to nil
|
|
352
|
-
let endingCallId = self.currentActiveCallId
|
|
353
|
-
print("Call ending: \(String(describing: endingCallId))")
|
|
354
|
-
|
|
355
|
-
// If newState is nil, hide overlay and clear call
|
|
356
|
-
self.overlayViewModel?.updateCall(nil)
|
|
357
|
-
self.overlayView?.isHidden = true
|
|
358
|
-
self.webView?.isOpaque = true
|
|
359
|
-
|
|
360
|
-
// Notify that call has ended - use the properly tracked call ID
|
|
361
|
-
self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
|
|
362
|
-
|
|
363
|
-
// Clean up any resources for this call
|
|
364
|
-
if let callCid = endingCallId {
|
|
365
|
-
// Invalidate and remove the timer
|
|
366
|
-
self.callStates[callCid]?.timer?.invalidate()
|
|
367
|
-
|
|
368
|
-
// Remove call from callStates
|
|
369
|
-
self.callStates.removeValue(forKey: callCid)
|
|
370
|
-
|
|
371
|
-
print("Cleaned up resources for ended call: \(callCid)")
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Clear the active call ID
|
|
375
|
-
self.currentActiveCallId = nil
|
|
376
|
-
}
|
|
377
|
-
} catch {
|
|
378
|
-
log.error("Error handling call state update: \(String(describing: error))")
|
|
379
|
-
}
|
|
286
|
+
|
|
287
|
+
// Check if we're in a state where we need the subscription but it's not active
|
|
288
|
+
if self.state == .initialized && self.activeCallSubscription == nil && self.callViewModel != nil {
|
|
289
|
+
print("Subscription check: Restoring lost activeCallSubscription")
|
|
290
|
+
self.setupActiveCallSubscription()
|
|
291
|
+
} else {
|
|
292
|
+
// Schedule the next check
|
|
293
|
+
self.scheduleSubscriptionCheck()
|
|
380
294
|
}
|
|
381
295
|
}
|
|
382
296
|
}
|
|
@@ -456,7 +370,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
456
370
|
|
|
457
371
|
// Update UI
|
|
458
372
|
await MainActor.run {
|
|
459
|
-
self.overlayViewModel?.updateCall(nil)
|
|
373
|
+
// self.overlayViewModel?.updateCall(nil)
|
|
460
374
|
self.overlayView?.isHidden = true
|
|
461
375
|
self.webView?.isOpaque = true
|
|
462
376
|
self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "timeout")
|
|
@@ -501,7 +415,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
501
415
|
// Remove from callStates
|
|
502
416
|
self.callStates.removeValue(forKey: callCid)
|
|
503
417
|
|
|
504
|
-
self.overlayViewModel?.updateCall(nil)
|
|
418
|
+
// self.overlayViewModel?.updateCall(nil)
|
|
505
419
|
self.overlayView?.isHidden = true
|
|
506
420
|
self.webView?.isOpaque = true
|
|
507
421
|
self.updateCallStatusAndNotify(callId: callCid, state: "ended", reason: "all_rejected_or_missed")
|
|
@@ -537,7 +451,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
537
451
|
|
|
538
452
|
// Update the CallOverlayView with new StreamVideo instance
|
|
539
453
|
Task { @MainActor in
|
|
540
|
-
self.overlayViewModel?.updateStreamVideo(self.streamVideo)
|
|
454
|
+
// self.overlayViewModel?.updateStreamVideo(self.streamVideo)
|
|
541
455
|
}
|
|
542
456
|
|
|
543
457
|
call.resolve([
|
|
@@ -571,8 +485,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
571
485
|
|
|
572
486
|
// Update the CallOverlayView with nil StreamVideo instance
|
|
573
487
|
Task { @MainActor in
|
|
574
|
-
self.overlayViewModel?.updateCall(nil)
|
|
575
|
-
self.overlayViewModel?.updateStreamVideo(nil)
|
|
488
|
+
// self.overlayViewModel?.updateCall(nil)
|
|
489
|
+
// self.overlayViewModel?.updateStreamVideo(nil)
|
|
576
490
|
self.overlayView?.isHidden = true
|
|
577
491
|
self.webView?.isOpaque = true
|
|
578
492
|
}
|
|
@@ -655,7 +569,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
655
569
|
|
|
656
570
|
// Update the CallOverlayView with the active call
|
|
657
571
|
await MainActor.run {
|
|
658
|
-
self.overlayViewModel?.updateCall(streamCall)
|
|
572
|
+
// self.overlayViewModel?.updateCall(streamCall)
|
|
659
573
|
self.overlayView?.isHidden = false
|
|
660
574
|
self.webView?.isOpaque = false
|
|
661
575
|
}
|
|
@@ -683,7 +597,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
683
597
|
|
|
684
598
|
// Update view state instead of cleaning up
|
|
685
599
|
await MainActor.run {
|
|
686
|
-
self.overlayViewModel?.updateCall(nil)
|
|
600
|
+
// self.overlayViewModel?.updateCall(nil)
|
|
687
601
|
self.overlayView?.isHidden = true
|
|
688
602
|
self.webView?.isOpaque = true
|
|
689
603
|
}
|
|
@@ -782,13 +696,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
782
696
|
|
|
783
697
|
// Join the call
|
|
784
698
|
print("Accepting and joining call \(streamCall!.cId)...")
|
|
785
|
-
|
|
786
|
-
|
|
699
|
+
guard case .incoming(let incomingCall) = await self.callViewModel?.callingState else {
|
|
700
|
+
call.reject("Failed to accept call as there is no call ID")
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
await self.callViewModel?.acceptCall(callType: incomingCall.type, callId: incomingCall.id)
|
|
787
705
|
print("Successfully joined call")
|
|
788
706
|
|
|
789
707
|
// Update the CallOverlayView with the active call
|
|
790
708
|
await MainActor.run {
|
|
791
|
-
self.overlayViewModel?.updateCall(streamCall)
|
|
709
|
+
// self.overlayViewModel?.updateCall(streamCall)
|
|
792
710
|
self.overlayView?.isHidden = false
|
|
793
711
|
self.webView?.isOpaque = false
|
|
794
712
|
}
|
|
@@ -807,6 +725,21 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
807
725
|
}
|
|
808
726
|
|
|
809
727
|
private func initializeStreamVideo() {
|
|
728
|
+
if (state == .initialized) {
|
|
729
|
+
print("initializeStreamVideo already initialized")
|
|
730
|
+
// Try to get user credentials from repository
|
|
731
|
+
guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
|
|
732
|
+
print("Save credentials not found, skipping initialization")
|
|
733
|
+
return
|
|
734
|
+
}
|
|
735
|
+
if (savedCredentials.user.id == streamVideo?.user.id) {
|
|
736
|
+
print("Skipping initializeStreamVideo as user is already logged in")
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
} else if (state == .initializing) {
|
|
740
|
+
print("initializeStreamVideo rejected - already initializing")
|
|
741
|
+
return
|
|
742
|
+
}
|
|
810
743
|
state = .initializing
|
|
811
744
|
|
|
812
745
|
// Try to get user credentials from repository
|
|
@@ -818,11 +751,31 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
818
751
|
}
|
|
819
752
|
print("Initializing with saved credentials for user: \(savedCredentials.user.name)")
|
|
820
753
|
|
|
754
|
+
LogConfig.level = .debug
|
|
821
755
|
self.streamVideo = StreamVideo(
|
|
822
756
|
apiKey: apiKey,
|
|
823
757
|
user: savedCredentials.user,
|
|
824
|
-
token: UserToken(stringLiteral: savedCredentials.tokenValue)
|
|
758
|
+
token: UserToken(stringLiteral: savedCredentials.tokenValue),
|
|
759
|
+
tokenProvider: {completion in
|
|
760
|
+
guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
|
|
761
|
+
print("No saved credentials or API key found, cannot refresh token")
|
|
762
|
+
|
|
763
|
+
completion(.failure(NSError(domain: "No saved credentials or API key found, cannot refresh token", code: 0, userInfo: nil)))
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
completion(.success(UserToken(stringLiteral: savedCredentials.tokenValue)))
|
|
767
|
+
}
|
|
825
768
|
)
|
|
769
|
+
|
|
770
|
+
if (self.callViewModel == nil) {
|
|
771
|
+
// Initialize on main thread with proper MainActor isolation
|
|
772
|
+
DispatchQueue.main.async {
|
|
773
|
+
Task { @MainActor in
|
|
774
|
+
self.callViewModel = CallViewModel(participantsLayout: .grid)
|
|
775
|
+
self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
826
779
|
|
|
827
780
|
state = .initialized
|
|
828
781
|
callKitAdapter.streamVideo = self.streamVideo
|
|
@@ -837,63 +790,98 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
837
790
|
}
|
|
838
791
|
|
|
839
792
|
private func setupViews() {
|
|
840
|
-
guard let webView = self.webView,
|
|
841
|
-
let parent = webView.superview else { return }
|
|
842
793
|
|
|
843
|
-
// Create TouchInterceptView
|
|
844
|
-
let touchInterceptView = TouchInterceptView(frame: parent.bounds)
|
|
845
|
-
touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
|
|
846
|
-
self.touchInterceptView = touchInterceptView
|
|
847
794
|
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
795
|
+
// // Create SwiftUI view with view model
|
|
796
|
+
// let (hostingController, viewModel) = CallOverlayView.create(streamVideo: self.streamVideo)
|
|
797
|
+
// hostingController.view.backgroundColor = .clear
|
|
798
|
+
// hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
799
|
+
//
|
|
800
|
+
// self.hostingController = hostingController
|
|
801
|
+
// self.overlayViewModel = viewModel
|
|
802
|
+
// self.overlayView = hostingController.view
|
|
803
|
+
//
|
|
804
|
+
// if let overlayView = self.overlayView {
|
|
805
|
+
// // Setup the views in TouchInterceptView
|
|
806
|
+
// touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
|
|
807
|
+
//
|
|
808
|
+
// // Setup constraints for webView
|
|
809
|
+
// NSLayoutConstraint.activate([
|
|
810
|
+
// webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
|
|
811
|
+
// webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
|
|
812
|
+
// webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
|
|
813
|
+
// webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
|
|
814
|
+
// ])
|
|
815
|
+
//
|
|
816
|
+
// // Setup constraints for overlayView
|
|
817
|
+
// let safeGuide = touchInterceptView.safeAreaLayoutGuide
|
|
818
|
+
// NSLayoutConstraint.activate([
|
|
819
|
+
// overlayView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
|
|
820
|
+
// overlayView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
|
|
821
|
+
// overlayView.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
|
|
822
|
+
// overlayView.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
|
|
823
|
+
// ])
|
|
824
|
+
// }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private func createCallOverlayView() {
|
|
828
|
+
guard let webView = self.webView,
|
|
829
|
+
let parent = webView.superview,
|
|
830
|
+
let callOverlayView = self.callViewModel else { return }
|
|
831
|
+
|
|
832
|
+
// Check if we already have an overlay view - do nothing if it exists
|
|
833
|
+
if let existingOverlayView = self.overlayView, existingOverlayView.superview != nil {
|
|
834
|
+
print("Call overlay view already exists, doing nothing")
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
print("Creating new call overlay view")
|
|
839
|
+
|
|
840
|
+
// First, create the overlay view
|
|
841
|
+
let overlayView = CallOverlayView.create(callViewModel: callOverlayView)
|
|
842
|
+
overlayView.view.translatesAutoresizingMaskIntoConstraints = false
|
|
843
|
+
|
|
844
|
+
// Important: Insert the overlay view BELOW the webView in the view hierarchy
|
|
845
|
+
parent.insertSubview(overlayView.view, belowSubview: webView)
|
|
846
|
+
|
|
847
|
+
// Set constraints to fill the parent's safe area
|
|
848
|
+
let safeGuide = parent.safeAreaLayoutGuide
|
|
849
|
+
|
|
855
850
|
NSLayoutConstraint.activate([
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
851
|
+
overlayView.view.topAnchor.constraint(equalTo: safeGuide.topAnchor),
|
|
852
|
+
overlayView.view.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),
|
|
853
|
+
overlayView.view.leadingAnchor.constraint(equalTo: safeGuide.leadingAnchor),
|
|
854
|
+
overlayView.view.trailingAnchor.constraint(equalTo: safeGuide.trailingAnchor)
|
|
860
855
|
])
|
|
861
|
-
|
|
862
|
-
//
|
|
863
|
-
webView.isOpaque =
|
|
856
|
+
|
|
857
|
+
// Set opacity for visual effect - make webView transparent to see overlay
|
|
858
|
+
webView.isOpaque = false
|
|
864
859
|
webView.backgroundColor = .clear
|
|
865
860
|
webView.scrollView.backgroundColor = .clear
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
872
|
-
|
|
873
|
-
self.hostingController = hostingController
|
|
874
|
-
self.overlayViewModel = viewModel
|
|
875
|
-
self.overlayView = hostingController.view
|
|
876
|
-
|
|
877
|
-
if let overlayView = self.overlayView {
|
|
878
|
-
// Setup the views in TouchInterceptView
|
|
879
|
-
touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
|
|
880
|
-
|
|
881
|
-
// Setup constraints for webView
|
|
882
|
-
NSLayoutConstraint.activate([
|
|
883
|
-
webView.topAnchor.constraint(equalTo: touchInterceptView.topAnchor),
|
|
884
|
-
webView.bottomAnchor.constraint(equalTo: touchInterceptView.bottomAnchor),
|
|
885
|
-
webView.leadingAnchor.constraint(equalTo: touchInterceptView.leadingAnchor),
|
|
886
|
-
webView.trailingAnchor.constraint(equalTo: touchInterceptView.trailingAnchor)
|
|
887
|
-
])
|
|
861
|
+
|
|
862
|
+
// Store reference to the hosting controller
|
|
863
|
+
self.hostingController = overlayView
|
|
864
|
+
self.overlayView = overlayView.view
|
|
865
|
+
}
|
|
888
866
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
867
|
+
private func ensureViewRemoved() {
|
|
868
|
+
// Check if we have an overlay view
|
|
869
|
+
if let existingOverlayView = self.overlayView {
|
|
870
|
+
print("Removing call overlay view")
|
|
871
|
+
|
|
872
|
+
// Remove the view from its superview
|
|
873
|
+
existingOverlayView.removeFromSuperview()
|
|
874
|
+
|
|
875
|
+
// Reset opacity for webView
|
|
876
|
+
self.webView?.isOpaque = true
|
|
877
|
+
self.webView?.backgroundColor = nil
|
|
878
|
+
self.webView?.scrollView.backgroundColor = nil
|
|
879
|
+
|
|
880
|
+
// Clear references
|
|
881
|
+
self.overlayView = nil
|
|
882
|
+
self.hostingController = nil
|
|
883
|
+
} else {
|
|
884
|
+
print("No call overlay view to remove")
|
|
897
885
|
}
|
|
898
886
|
}
|
|
899
887
|
|
package/package.json
CHANGED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
|
|
3
|
-
class TouchInterceptView: UIView {
|
|
4
|
-
private var webView: UIView?
|
|
5
|
-
private var overlayView: UIView?
|
|
6
|
-
private var labeledFrames: [ViewFramePreferenceData] = []
|
|
7
|
-
|
|
8
|
-
override init(frame: CGRect) {
|
|
9
|
-
super.init(frame: frame)
|
|
10
|
-
isUserInteractionEnabled = true
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
required init?(coder: NSCoder) {
|
|
14
|
-
super.init(coder: coder)
|
|
15
|
-
isUserInteractionEnabled = true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func setupWithWebView(_ webView: UIView, overlayView: UIView) {
|
|
19
|
-
self.webView = webView
|
|
20
|
-
self.overlayView = overlayView
|
|
21
|
-
|
|
22
|
-
// Add both views as subviews
|
|
23
|
-
addSubview(overlayView)
|
|
24
|
-
addSubview(webView)
|
|
25
|
-
|
|
26
|
-
// Ensure both views can receive touches
|
|
27
|
-
webView.isUserInteractionEnabled = true
|
|
28
|
-
overlayView.isUserInteractionEnabled = true
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
32
|
-
guard let webView = webView,
|
|
33
|
-
let overlayView = overlayView else {
|
|
34
|
-
super.touchesBegan(touches, with: event)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Convert touch locations and check labeled frames
|
|
39
|
-
if let touch = touches.first {
|
|
40
|
-
let point = touch.location(in: self)
|
|
41
|
-
let globalPoint = convert(point, to: nil)
|
|
42
|
-
|
|
43
|
-
// If touch is in a labeled frame, only send to overlay
|
|
44
|
-
if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
|
|
45
|
-
overlayView.touchesBegan(touches, with: event)
|
|
46
|
-
} else {
|
|
47
|
-
// Otherwise broadcast to both views
|
|
48
|
-
webView.touchesBegan(touches, with: event)
|
|
49
|
-
overlayView.touchesBegan(touches, with: event)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
super.touchesBegan(touches, with: event)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
57
|
-
guard let webView = webView,
|
|
58
|
-
let overlayView = overlayView else {
|
|
59
|
-
super.touchesMoved(touches, with: event)
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if let touch = touches.first {
|
|
64
|
-
let point = touch.location(in: self)
|
|
65
|
-
let globalPoint = convert(point, to: nil)
|
|
66
|
-
|
|
67
|
-
if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
|
|
68
|
-
overlayView.touchesMoved(touches, with: event)
|
|
69
|
-
} else {
|
|
70
|
-
webView.touchesMoved(touches, with: event)
|
|
71
|
-
overlayView.touchesMoved(touches, with: event)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
super.touchesMoved(touches, with: event)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
79
|
-
guard let webView = webView,
|
|
80
|
-
let overlayView = overlayView else {
|
|
81
|
-
super.touchesEnded(touches, with: event)
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if let touch = touches.first {
|
|
86
|
-
let point = touch.location(in: self)
|
|
87
|
-
let globalPoint = convert(point, to: nil)
|
|
88
|
-
|
|
89
|
-
if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
|
|
90
|
-
overlayView.touchesEnded(touches, with: event)
|
|
91
|
-
} else {
|
|
92
|
-
webView.touchesEnded(touches, with: event)
|
|
93
|
-
overlayView.touchesEnded(touches, with: event)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
super.touchesEnded(touches, with: event)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
101
|
-
guard let webView = webView,
|
|
102
|
-
let overlayView = overlayView else {
|
|
103
|
-
super.touchesCancelled(touches, with: event)
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if let touch = touches.first {
|
|
108
|
-
let point = touch.location(in: self)
|
|
109
|
-
let globalPoint = convert(point, to: nil)
|
|
110
|
-
|
|
111
|
-
if labeledFrames.contains(where: { $0.frame.contains(globalPoint) }) {
|
|
112
|
-
overlayView.touchesCancelled(touches, with: event)
|
|
113
|
-
} else {
|
|
114
|
-
webView.touchesCancelled(touches, with: event)
|
|
115
|
-
overlayView.touchesCancelled(touches, with: event)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
super.touchesCancelled(touches, with: event)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
123
|
-
return true
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
127
|
-
guard let webView = webView,
|
|
128
|
-
let overlayView = overlayView else {
|
|
129
|
-
return super.hitTest(point, with: event)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Convert point to global coordinates for labeled frame checking
|
|
133
|
-
let globalPoint = convert(point, to: nil)
|
|
134
|
-
|
|
135
|
-
// print("TouchInterceptView - Hit test at global point: \(globalPoint)")
|
|
136
|
-
// print("Current labeled frames: \(labeledFrames.map { "\($0.label): \($0.frame)" }.joined(separator: ", "))")
|
|
137
|
-
|
|
138
|
-
// Convert point for both views
|
|
139
|
-
let webViewPoint = convert(point, to: webView)
|
|
140
|
-
let overlayPoint = convert(point, to: overlayView)
|
|
141
|
-
|
|
142
|
-
// First check if the point is inside any labeled frame
|
|
143
|
-
for labeledFrame in labeledFrames {
|
|
144
|
-
if labeledFrame.frame.contains(globalPoint) {
|
|
145
|
-
// print("Hit labeled frame: \(labeledFrame.label)")
|
|
146
|
-
// If it's in a labeled frame, let the overlay handle it
|
|
147
|
-
if overlayView.point(inside: overlayPoint, with: event),
|
|
148
|
-
let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
|
|
149
|
-
return overlayHitView
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// If not in a labeled frame, let webview try first
|
|
155
|
-
if webView.point(inside: webViewPoint, with: event),
|
|
156
|
-
let webViewHitView = webView.hitTest(webViewPoint, with: event) {
|
|
157
|
-
return webViewHitView
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Finally check if overlay wants to handle the touch
|
|
161
|
-
if overlayView.point(inside: overlayPoint, with: event),
|
|
162
|
-
let overlayHitView = overlayView.hitTest(overlayPoint, with: event) {
|
|
163
|
-
return overlayHitView
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return super.hitTest(point, with: event)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
func updateLabeledFrames(_ frames: [ViewFramePreferenceData]) {
|
|
170
|
-
print("TouchInterceptView - Updating labeled frames:")
|
|
171
|
-
print("Number of frames: \(frames.count)")
|
|
172
|
-
frames.forEach { frame in
|
|
173
|
-
print("Label: \(frame.label), Frame: \(frame.frame)")
|
|
174
|
-
}
|
|
175
|
-
self.labeledFrames = frames
|
|
176
|
-
}
|
|
177
|
-
}
|