Skip to content

Commit 1fb40f1

Browse files
authored
[Feature]Disconnection timeout during call (#573)
1 parent b7766ec commit 1fb40f1

File tree

23 files changed

+678
-70
lines changed

23 files changed

+678
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
### ✅ Added
88
- You can now provide the incoming video quality setting for some or all participants [#571](https://github.com/GetStream/stream-video-swift/pull/571)
9+
- You can now set the time a user can remain in the call - after their connection disrupted - while waiting for their network connection to recover [#573](https://github.com/GetStream/stream-video-swift/pull/573)
910

1011
# [1.13.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.13.0)
1112
_October 08, 2024_

DemoApp/Sources/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
9999
.current()
100100
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
101101
if granted {
102-
DispatchQueue.main.async {
102+
Task { @MainActor in
103103
UIApplication.shared.registerForRemoteNotifications()
104104
}
105105
}

DemoApp/Sources/Components/AppEnvironment.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,36 @@ extension AppEnvironment {
442442

443443
static var autoLeavePolicy: AutoLeavePolicy = .default
444444
}
445+
446+
extension AppEnvironment {
447+
448+
enum DisconnectionTimeout: Hashable, Debuggable {
449+
case never
450+
case twoMinutes
451+
case custom(TimeInterval)
452+
453+
var title: String {
454+
switch self {
455+
case .never:
456+
return "Never"
457+
case .twoMinutes:
458+
return "2'"
459+
case let .custom(value):
460+
return "\(value)\""
461+
}
462+
}
463+
464+
var duration: TimeInterval {
465+
switch self {
466+
case .never:
467+
return 0
468+
case .twoMinutes:
469+
return 2 * 60
470+
case let .custom(value):
471+
return value
472+
}
473+
}
474+
}
475+
476+
static var disconnectionTimeout: DisconnectionTimeout = .never
477+
}

DemoApp/Sources/Components/Feedback/DemoFeedbackView.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct DemoFeedbackView: View {
1616
@State private var comment: String = ""
1717
@State private var rating: Int = 5
1818
@State private var isSubmitting = false
19+
@State private var toast: Toast?
1920

2021
private weak var call: Call?
2122
private var dismiss: () -> Void
@@ -121,6 +122,20 @@ struct DemoFeedbackView: View {
121122
.padding(.horizontal)
122123
}
123124
.withModalNavigationBar(title: "", closeAction: dismiss)
125+
.toastView(toast: $toast)
126+
.onAppear { checkIfDisconnectionErrorIsAvailable() }
127+
}
128+
129+
// MARK: - Private helpers
130+
131+
@MainActor
132+
func checkIfDisconnectionErrorIsAvailable() {
133+
if call?.state.disconnectionError is ClientError.NetworkNotAvailable {
134+
toast = .init(
135+
style: .error,
136+
message: "Your call was ended because it seems your internet connection is down."
137+
)
138+
}
124139
}
125140
}
126141

DemoApp/Sources/Views/CallView/CallingView/DemoCallingViewModifier.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ struct DemoCallingViewModifier: ViewModifier {
7272
}
7373
.onReceive(appState.$activeCall) { call in
7474
viewModel.setActiveCall(call)
75+
call?.setDisconnectionTimeout(AppEnvironment.disconnectionTimeout.duration)
7576
}
7677
.onReceive(appState.$userState) { userState in
7778
if userState == .notLoggedIn {

DemoApp/Sources/Views/Login/DebugMenu.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ struct DebugMenu: View {
7676
didSet { AppEnvironment.callExpiration = callExpiration }
7777
}
7878

79+
@State private var disconnectionTimeout: AppEnvironment.DisconnectionTimeout = AppEnvironment.disconnectionTimeout {
80+
didSet { AppEnvironment.disconnectionTimeout = disconnectionTimeout }
81+
}
82+
7983
@State private var isLogsViewerVisible: Bool = false
8084

8185
@State private var presentsCustomEnvironmentSetup: Bool = false
@@ -86,6 +90,9 @@ struct DebugMenu: View {
8690
@State private var customTokenExpirationValue: Int = 0
8791
@State private var presentsCustomTokenExpiration: Bool = false
8892

93+
@State private var customDisconnectionTimeoutValue: TimeInterval = 0
94+
@State private var presentsCustomDisconnectionTimeout: Bool = false
95+
8996
@State private var autoLeavePolicy: AppEnvironment.AutoLeavePolicy = AppEnvironment.autoLeavePolicy {
9097
didSet { AppEnvironment.autoLeavePolicy = autoLeavePolicy }
9198
}
@@ -149,6 +156,13 @@ struct DebugMenu: View {
149156
label: "Auto Leave policy"
150157
) { self.autoLeavePolicy = $0 }
151158

159+
makeMenu(
160+
for: [.never, .twoMinutes],
161+
currentValue: disconnectionTimeout,
162+
additionalItems: { customDisconnectionTimeoutView },
163+
label: "Disconnection Timeout"
164+
) { self.disconnectionTimeout = $0 }
165+
152166
makeMenu(
153167
for: [.visible, .hidden],
154168
currentValue: performanceTrackerVisibility,
@@ -213,6 +227,14 @@ struct DebugMenu: View {
213227
transformer: { Int($0) ?? 0 },
214228
action: { self.tokenExpiration = .custom(customTokenExpirationValue) }
215229
)
230+
.alertWithTextField(
231+
title: "Enter disconnection timeout in seconds",
232+
placeholder: "Interval",
233+
presentationBinding: $presentsCustomDisconnectionTimeout,
234+
valueBinding: $customDisconnectionTimeoutValue,
235+
transformer: { TimeInterval($0) ?? 0 },
236+
action: { self.disconnectionTimeout = .custom(customDisconnectionTimeoutValue) }
237+
)
216238
}
217239

218240
@ViewBuilder
@@ -290,6 +312,31 @@ struct DebugMenu: View {
290312
}
291313
}
292314

315+
@ViewBuilder
316+
private var customDisconnectionTimeoutView: some View {
317+
if case let .custom(value) = AppEnvironment.disconnectionTimeout {
318+
Button {
319+
presentsCustomDisconnectionTimeout = true
320+
} label: {
321+
Label {
322+
Text("Custom (\(value)\")")
323+
} icon: {
324+
Image(systemName: "checkmark")
325+
}
326+
}
327+
} else {
328+
Button {
329+
presentsCustomDisconnectionTimeout = true
330+
} label: {
331+
Label {
332+
Text("Custom")
333+
} icon: {
334+
EmptyView()
335+
}
336+
}
337+
}
338+
}
339+
293340
@ViewBuilder
294341
private func makeMenu<Item: Debuggable>(
295342
for items: [Item],

DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
400D91D12B63DEA200EBA47D /* 04-camera-and-microphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D02B63DEA200EBA47D /* 04-camera-and-microphone.swift */; };
1717
400D91D32B63DFA500EBA47D /* 06-querying-calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D22B63DFA500EBA47D /* 06-querying-calls.swift */; };
1818
400D91D52B63E27300EBA47D /* 07-dependency-injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D42B63E27300EBA47D /* 07-dependency-injection.swift */; };
19-
4029E95E2CB94EAE00E1D571 /* 21-manual-quality-selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */; };
19+
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */; };
2020
404CAED82B8E3874007087BC /* 06-apply-video-filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */; };
2121
4068C1252B67C056006B0BEE /* 03-callkit-integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */; };
2222
408CE0F52BD91B490052EC3A /* 19-transcriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */; };
23+
409774B02CC19F5500E0D3EE /* 23-network-disruption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */; };
2324
409C39692B67CC5C0090044C /* 04-screensharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C39682B67CC5C0090044C /* 04-screensharing.swift */; };
2425
409C396B2B67CD0B0090044C /* 05-picture-in-picture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C396A2B67CD0B0090044C /* 05-picture-in-picture.swift */; };
2526
409C396D2B67CD780090044C /* 08-recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C396C2B67CD780090044C /* 08-recording.swift */; };
@@ -94,10 +95,11 @@
9495
400D91D02B63DEA200EBA47D /* 04-camera-and-microphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-camera-and-microphone.swift"; sourceTree = "<group>"; };
9596
400D91D22B63DFA500EBA47D /* 06-querying-calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-querying-calls.swift"; sourceTree = "<group>"; };
9697
400D91D42B63E27300EBA47D /* 07-dependency-injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "07-dependency-injection.swift"; sourceTree = "<group>"; };
97-
4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "21-manual-quality-selection.swift"; sourceTree = "<group>"; };
98+
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "22-manual-quality-selection.swift"; sourceTree = "<group>"; };
9899
404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-apply-video-filters.swift"; sourceTree = "<group>"; };
99100
4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-callkit-integration.swift"; sourceTree = "<group>"; };
100101
408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "19-transcriptions.swift"; sourceTree = "<group>"; };
102+
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "23-network-disruption.swift"; sourceTree = "<group>"; };
101103
409C39682B67CC5C0090044C /* 04-screensharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-screensharing.swift"; sourceTree = "<group>"; };
102104
409C396A2B67CD0B0090044C /* 05-picture-in-picture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-picture-in-picture.swift"; sourceTree = "<group>"; };
103105
409C396C2B67CD780090044C /* 08-recording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-recording.swift"; sourceTree = "<group>"; };
@@ -274,7 +276,8 @@
274276
408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */,
275277
40F18B8F2BEBC97F00ADF76E /* 18-call-quality-rating.swift */,
276278
40F290AC2BDFB3CA00DCF136 /* 20-noise-cancellation.swift */,
277-
4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */,
279+
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */,
280+
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */,
278281
);
279282
path = "05-ui-cookbook";
280283
sourceTree = "<group>";
@@ -465,12 +468,13 @@
465468
4068C1252B67C056006B0BEE /* 03-callkit-integration.swift in Sources */,
466469
40FFDC762B63F7D6004DA7A2 /* ChatGloballyUsedVariables.swift in Sources */,
467470
84BA15AE2CA2EF420018DC51 /* 07-querying-call-members.swift in Sources */,
471+
409774B02CC19F5500E0D3EE /* 23-network-disruption.swift in Sources */,
468472
40FFDC672B63F430004DA7A2 /* 04-connection-quality-indicator.swift in Sources */,
469473
40B468982B67B6DF009B5B3E /* 01-deeplinking.swift in Sources */,
470474
40FFDC3B2B63E493004DA7A2 /* 10-view-slots.swift in Sources */,
471475
40FFDC442B63E95D004DA7A2 /* 14-swiftui-vs-uikit.swift in Sources */,
472476
40FFDC872B63FEAE004DA7A2 /* 05-incoming-call.swift in Sources */,
473-
4029E95E2CB94EAE00E1D571 /* 21-manual-quality-selection.swift in Sources */,
477+
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */,
474478
40FFDC942B6401CC004DA7A2 /* 07-video-fallback.swift in Sources */,
475479
400D91C72B63D96800EBA47D /* 03-quickstart.swift in Sources */,
476480
40FFDC9E2B64063D004DA7A2 /* 12-connection-unstable.swift in Sources */,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import StreamVideo
2+
import StreamVideoSwiftUI
3+
import SwiftUI
4+
import Combine
5+
6+
@MainActor
7+
fileprivate func content() {
8+
container {
9+
let call = streamVideo.call(callType: "default", callId: callId)
10+
11+
// Set the disconnection timeout to 60 seconds
12+
call.setDisconnectionTimeout(60)
13+
}
14+
15+
container {
16+
struct DemoFeedbackView: View {
17+
18+
@Environment(\.openURL) private var openURL
19+
@Injected(\.appearance) private var appearance
20+
21+
@State private var email: String = ""
22+
@State private var comment: String = ""
23+
@State private var rating: Int = 5
24+
@State private var isSubmitting = false
25+
@State private var toast: Toast?
26+
27+
private weak var call: Call?
28+
private var dismiss: () -> Void
29+
private var isSubmitEnabled: Bool { !email.isEmpty && !isSubmitting }
30+
31+
init(_ call: Call, dismiss: @escaping () -> Void) {
32+
self.call = call
33+
self.dismiss = dismiss
34+
}
35+
36+
var body: some View {
37+
ScrollView {
38+
VStack(spacing: 32) {
39+
Image("feedbackLogo")
40+
41+
VStack(spacing: 8) {
42+
Text("How is your call going?")
43+
.font(appearance.fonts.headline)
44+
.foregroundColor(appearance.colors.text)
45+
.lineLimit(1)
46+
47+
Text("All feedback is celebrated!")
48+
.font(appearance.fonts.subheadline)
49+
.foregroundColor(.init(appearance.colors.textLowEmphasis))
50+
.lineLimit(2)
51+
}
52+
.frame(maxWidth: .infinity, alignment: .center)
53+
.multilineTextAlignment(.center)
54+
55+
VStack(spacing: 27) {
56+
VStack(spacing: 16) {
57+
TextField(
58+
"Email Address *",
59+
text: $email
60+
)
61+
.textFieldStyle(DemoTextfieldStyle())
62+
63+
DemoTextEditor(text: $comment, placeholder: "Message")
64+
}
65+
66+
HStack {
67+
Text("Rate Quality")
68+
.font(appearance.fonts.body)
69+
.foregroundColor(.init(appearance.colors.textLowEmphasis))
70+
.frame(maxWidth: .infinity, alignment: .leading)
71+
72+
DemoStarRatingView(rating: $rating)
73+
}
74+
}
75+
76+
HStack {
77+
Button {
78+
resignFirstResponder()
79+
openURL(.init(string: "https://getstream.io/video/#contact")!)
80+
} label: {
81+
Text("Contact Us")
82+
}
83+
.frame(maxWidth: .infinity)
84+
.foregroundColor(appearance.colors.text)
85+
.padding(.vertical, 4)
86+
.clipShape(Capsule())
87+
.overlay(Capsule().stroke(Color(appearance.colors.textLowEmphasis), lineWidth: 1))
88+
89+
Button {
90+
resignFirstResponder()
91+
isSubmitting = true
92+
Task {
93+
do {
94+
try await call?.collectUserFeedback(
95+
rating: rating,
96+
reason: """
97+
\(email)
98+
\(comment)
99+
"""
100+
)
101+
Task { @MainActor in
102+
dismiss()
103+
}
104+
isSubmitting = false
105+
} catch {
106+
log.error(error)
107+
dismiss()
108+
isSubmitting = false
109+
}
110+
}
111+
} label: {
112+
if isSubmitting {
113+
ProgressView()
114+
} else {
115+
Text("Submit")
116+
}
117+
}
118+
.frame(maxWidth: .infinity)
119+
.foregroundColor(appearance.colors.text)
120+
.padding(.vertical, 4)
121+
.background(isSubmitEnabled ? appearance.colors.accentBlue : appearance.colors.lightGray)
122+
.disabled(!isSubmitEnabled)
123+
.clipShape(Capsule())
124+
}
125+
126+
Spacer()
127+
}
128+
.padding(.horizontal)
129+
}
130+
.toastView(toast: $toast)
131+
.onAppear { checkIfDisconnectionErrorIsAvailable() }
132+
}
133+
134+
// MARK: - Private helpers
135+
136+
@MainActor
137+
func checkIfDisconnectionErrorIsAvailable() {
138+
if call?.state.disconnectionError is ClientError.NetworkNotAvailable {
139+
toast = .init(
140+
style: .error,
141+
message: "Your call was ended because it seems your internet connection is down."
142+
)
143+
}
144+
}
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)