MSC4075 Use expirationTS to define the call ringing window (#4652)
* Listen to call decline to stop ringing when declined from other device * MSC4075 Use expirationTS to define the call ringing window * Implement ElementCallService tests. * Update acknowledgements.
This commit is contained in:
@@ -344,6 +344,7 @@
|
||||
3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; };
|
||||
3CB9EC9B670C90618B839D1B /* RemotePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A05E85E4872C3221C5C287 /* RemotePreference.swift */; };
|
||||
3CE4C5071B6D2576E2473989 /* OrderedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B07B296D7A9D2F09120853 /* OrderedSet.swift */; };
|
||||
3D0DAED550E967AB49F1758C /* CXProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E68CA59F66CE43B66D129E9 /* CXProviderProtocol.swift */; };
|
||||
3D72F5F9109AAA257542456B /* CallInviteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */; };
|
||||
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; };
|
||||
3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */; };
|
||||
@@ -469,6 +470,7 @@
|
||||
53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; };
|
||||
53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; };
|
||||
53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; };
|
||||
5470E62F65AE1803BBF3D528 /* CXProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E1BAA7232081635662A83F /* CXProviderMock.swift */; };
|
||||
54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
||||
54FDA3625AACBD9E438D084D /* BlurEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07934EF08BB39353E4A94272 /* BlurEffectView.swift */; };
|
||||
5518DA4A6C9B4FC4B497EA9A /* LogViewerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B795AAAB7B8747FE2FF311 /* LogViewerScreenModels.swift */; };
|
||||
@@ -1014,6 +1016,7 @@
|
||||
B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; };
|
||||
B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */; };
|
||||
B5BCE012F9E7C45D1C76108E /* RoomMembersListScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */; };
|
||||
B5C40DCFFDFBA0F86E228602 /* Clocks in Frameworks */ = {isa = PBXBuildFile; productRef = FFA423B0A7BBD8AA9BB91AB0 /* Clocks */; };
|
||||
B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; };
|
||||
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||
B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; };
|
||||
@@ -1195,6 +1198,7 @@
|
||||
D63974A88CF2BC721F109C77 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = DCA3C4A997AD28E6918D4CE5 /* Compound */; };
|
||||
D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; };
|
||||
D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; };
|
||||
D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */; };
|
||||
D8459AAD6969B1431ECBE990 /* UnsupportedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */; };
|
||||
D8517B8EED58D24396FB71E7 /* DeactivateAccountScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BAE25A0E9E9F2F1BBA8930 /* DeactivateAccountScreenViewModel.swift */; };
|
||||
D885B783B95AD7832D4EF5DD /* CharacterSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8C01DEEA83903D45069BBD /* CharacterSet.swift */; };
|
||||
@@ -2003,6 +2007,7 @@
|
||||
5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
5E43D8784B0054C048060FEB /* LabsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenModels.swift; sourceTree = "<group>"; };
|
||||
5E5A65B5A000E7237AC61C67 /* LeaveSpaceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceViewModel.swift; sourceTree = "<group>"; };
|
||||
5E68CA59F66CE43B66D129E9 /* CXProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CXProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
5E6DE144D887A254F4CAF203 /* UserPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = "<group>"; };
|
||||
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = "<group>"; };
|
||||
@@ -2155,6 +2160,7 @@
|
||||
7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceTests.swift; sourceTree = "<group>"; };
|
||||
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInviterLabel.swift; sourceTree = "<group>"; };
|
||||
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -2212,6 +2218,7 @@
|
||||
86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
86C8CE2630F54D5FE1591786 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
86D7CD5CA270BFC3EBB450CA /* PinnedEventsTimelineScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
86E1BAA7232081635662A83F /* CXProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CXProviderMock.swift; sourceTree = "<group>"; };
|
||||
87FC42213E86E8182CFD3A49 /* preview_avatar_user.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_user.jpg; sourceTree = "<group>"; };
|
||||
88410BD213FDF9B28E8B671F /* UserDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreen.swift; sourceTree = "<group>"; };
|
||||
8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreen.swift; sourceTree = "<group>"; };
|
||||
@@ -2872,6 +2879,7 @@
|
||||
files = (
|
||||
7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */,
|
||||
BCA5E2157CE27AB6F1D043D3 /* AsyncAlgorithms in Frameworks */,
|
||||
B5C40DCFFDFBA0F86E228602 /* Clocks in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -3530,6 +3538,7 @@
|
||||
8F7FC9580CABF797A2E6213A /* BugReportServiceMock.swift */,
|
||||
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
|
||||
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
|
||||
86E1BAA7232081635662A83F /* CXProviderMock.swift */,
|
||||
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
|
||||
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
|
||||
867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */,
|
||||
@@ -4606,6 +4615,7 @@
|
||||
DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */,
|
||||
6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */,
|
||||
906451FB8CF27C628152BF7A /* EditRoomAddressScreenViewModelTests.swift */,
|
||||
7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */,
|
||||
A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */,
|
||||
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */,
|
||||
84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */,
|
||||
@@ -5183,6 +5193,7 @@
|
||||
92E99C57D7F92ED16F73282C /* ElementCall */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E68CA59F66CE43B66D129E9 /* CXProviderProtocol.swift */,
|
||||
CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */,
|
||||
33AE897D86784CCA5E4E9227 /* ElementCallService.swift */,
|
||||
406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */,
|
||||
@@ -6709,6 +6720,7 @@
|
||||
packageProductDependencies = (
|
||||
C07EA60CAB296D7726210F5B /* MatrixRustSDK */,
|
||||
5A8EF1A5F9629FCA309D4B2A /* AsyncAlgorithms */,
|
||||
FFA423B0A7BBD8AA9BB91AB0 /* Clocks */,
|
||||
);
|
||||
productName = UnitTests;
|
||||
productReference = AAC9344689121887B74877AF /* UnitTests.xctest */;
|
||||
@@ -6959,6 +6971,7 @@
|
||||
E025F19D013D9BA6C58B37F4 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
|
||||
AC3475112CA40C2C6E78D1EB /* XCRemoteSwiftPackageReference "matrix-analytics-events" */,
|
||||
4A8D3ABF18EABB8066BBD46E /* XCRemoteSwiftPackageReference "swift-async-algorithms" */,
|
||||
869B65C34E469FC879A9F116 /* XCRemoteSwiftPackageReference "swift-clocks" */,
|
||||
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
4C34425923978C97409A3EF2 /* XCRemoteSwiftPackageReference "DSWaveformImage" */,
|
||||
D5F7D47BBAAE0CF1DDEB3034 /* XCRemoteSwiftPackageReference "DeviceKit" */,
|
||||
@@ -7371,6 +7384,7 @@
|
||||
A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */,
|
||||
864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */,
|
||||
EDB6915EC953BB2A44AA608E /* EditRoomAddressScreenViewModelTests.swift in Sources */,
|
||||
D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */,
|
||||
7AE25D29734267271106D732 /* EmojiPickerScreenViewModelTests.swift in Sources */,
|
||||
25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */,
|
||||
71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */,
|
||||
@@ -7684,6 +7698,8 @@
|
||||
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */,
|
||||
E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */,
|
||||
6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */,
|
||||
5470E62F65AE1803BBF3D528 /* CXProviderMock.swift in Sources */,
|
||||
3D0DAED550E967AB49F1758C /* CXProviderProtocol.swift in Sources */,
|
||||
01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */,
|
||||
3D72F5F9109AAA257542456B /* CallInviteRoomTimelineView.swift in Sources */,
|
||||
E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */,
|
||||
@@ -9484,6 +9500,14 @@
|
||||
minimumVersion = 1.4.2;
|
||||
};
|
||||
};
|
||||
869B65C34E469FC879A9F116 /* XCRemoteSwiftPackageReference "swift-clocks" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pointfreeco/swift-clocks";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.6;
|
||||
};
|
||||
};
|
||||
91740346377FEBEAF7AD32FC /* XCRemoteSwiftPackageReference "swift-mutex" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/swhitty/swift-mutex";
|
||||
@@ -9916,6 +9940,11 @@
|
||||
package = AB8E808A59756170682BEC20 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
FFA423B0A7BBD8AA9BB91AB0 /* Clocks */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 869B65C34E469FC879A9F116 /* XCRemoteSwiftPackageReference "swift-clocks" */;
|
||||
productName = Clocks;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = AC22997D58D612146053154D /* Project object */;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"originHash" : "d68e23488bdd8328e4d65f4aa0fb826b3aaad601da516473abe6544c1a13c3f0",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "compound-design-tokens",
|
||||
@@ -216,6 +217,15 @@
|
||||
"version" : "1.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||
"state" : {
|
||||
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -225,6 +235,15 @@
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-concurrency-extras",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||
"state" : {
|
||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||
"version" : "1.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
17
ElementX/Sources/Mocks/CXProviderMock.swift
Normal file
17
ElementX/Sources/Mocks/CXProviderMock.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
extension CXProviderMock {
|
||||
struct Configuration { }
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
reportNewIncomingCallWithUpdateCompletionClosure = { _, _, completion in
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import AnalyticsEvents
|
||||
import AVFoundation
|
||||
import CallKit
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import Photos
|
||||
@@ -2066,6 +2067,132 @@ class BugReportServiceMock: BugReportServiceProtocol, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
class CXProviderMock: CXProviderProtocol, @unchecked Sendable {
|
||||
|
||||
//MARK: - setDelegate
|
||||
|
||||
var setDelegateQueueUnderlyingCallsCount = 0
|
||||
var setDelegateQueueCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return setDelegateQueueUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = setDelegateQueueUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
setDelegateQueueUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
setDelegateQueueUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var setDelegateQueueCalled: Bool {
|
||||
return setDelegateQueueCallsCount > 0
|
||||
}
|
||||
var setDelegateQueueReceivedArguments: (delegate: CXProviderDelegate?, queue: DispatchQueue?)?
|
||||
var setDelegateQueueReceivedInvocations: [(delegate: CXProviderDelegate?, queue: DispatchQueue?)] = []
|
||||
var setDelegateQueueClosure: ((CXProviderDelegate?, DispatchQueue?) -> Void)?
|
||||
|
||||
func setDelegate(_ delegate: CXProviderDelegate?, queue: DispatchQueue?) {
|
||||
setDelegateQueueCallsCount += 1
|
||||
setDelegateQueueReceivedArguments = (delegate: delegate, queue: queue)
|
||||
DispatchQueue.main.async {
|
||||
self.setDelegateQueueReceivedInvocations.append((delegate: delegate, queue: queue))
|
||||
}
|
||||
setDelegateQueueClosure?(delegate, queue)
|
||||
}
|
||||
//MARK: - reportNewIncomingCall
|
||||
|
||||
var reportNewIncomingCallWithUpdateCompletionUnderlyingCallsCount = 0
|
||||
var reportNewIncomingCallWithUpdateCompletionCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return reportNewIncomingCallWithUpdateCompletionUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = reportNewIncomingCallWithUpdateCompletionUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
reportNewIncomingCallWithUpdateCompletionUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
reportNewIncomingCallWithUpdateCompletionUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var reportNewIncomingCallWithUpdateCompletionCalled: Bool {
|
||||
return reportNewIncomingCallWithUpdateCompletionCallsCount > 0
|
||||
}
|
||||
var reportNewIncomingCallWithUpdateCompletionReceivedArguments: (uuid: UUID, update: CXCallUpdate, completion: (Error?) -> Void)?
|
||||
var reportNewIncomingCallWithUpdateCompletionReceivedInvocations: [(uuid: UUID, update: CXCallUpdate, completion: (Error?) -> Void)] = []
|
||||
var reportNewIncomingCallWithUpdateCompletionClosure: ((UUID, CXCallUpdate, @escaping (Error?) -> Void) -> Void)?
|
||||
|
||||
func reportNewIncomingCall(with uuid: UUID, update: CXCallUpdate, completion: @escaping (Error?) -> Void) {
|
||||
reportNewIncomingCallWithUpdateCompletionCallsCount += 1
|
||||
reportNewIncomingCallWithUpdateCompletionReceivedArguments = (uuid: uuid, update: update, completion: completion)
|
||||
DispatchQueue.main.async {
|
||||
self.reportNewIncomingCallWithUpdateCompletionReceivedInvocations.append((uuid: uuid, update: update, completion: completion))
|
||||
}
|
||||
reportNewIncomingCallWithUpdateCompletionClosure?(uuid, update, completion)
|
||||
}
|
||||
//MARK: - reportCall
|
||||
|
||||
var reportCallWithEndedAtReasonUnderlyingCallsCount = 0
|
||||
var reportCallWithEndedAtReasonCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return reportCallWithEndedAtReasonUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = reportCallWithEndedAtReasonUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
reportCallWithEndedAtReasonUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
reportCallWithEndedAtReasonUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var reportCallWithEndedAtReasonCalled: Bool {
|
||||
return reportCallWithEndedAtReasonCallsCount > 0
|
||||
}
|
||||
var reportCallWithEndedAtReasonReceivedArguments: (uuid: UUID, endedAt: Date?, reason: CXCallEndedReason)?
|
||||
var reportCallWithEndedAtReasonReceivedInvocations: [(uuid: UUID, endedAt: Date?, reason: CXCallEndedReason)] = []
|
||||
var reportCallWithEndedAtReasonClosure: ((UUID, Date?, CXCallEndedReason) -> Void)?
|
||||
|
||||
func reportCall(with uuid: UUID, endedAt: Date?, reason: CXCallEndedReason) {
|
||||
reportCallWithEndedAtReasonCallsCount += 1
|
||||
reportCallWithEndedAtReasonReceivedArguments = (uuid: uuid, endedAt: endedAt, reason: reason)
|
||||
DispatchQueue.main.async {
|
||||
self.reportCallWithEndedAtReasonReceivedInvocations.append((uuid: uuid, endedAt: endedAt, reason: reason))
|
||||
}
|
||||
reportCallWithEndedAtReasonClosure?(uuid, endedAt, reason)
|
||||
}
|
||||
}
|
||||
class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> {
|
||||
get { return underlyingActionsPublisher }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import CallKit
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol CXProviderProtocol {
|
||||
func setDelegate(_ delegate: CXProviderDelegate?, queue: DispatchQueue?)
|
||||
func reportNewIncomingCall(with uuid: UUID, update: CXCallUpdate, completion: @escaping (Error?) -> Void)
|
||||
func reportCall(with uuid: UUID, endedAt: Date?, reason: CXCallEndedReason)
|
||||
}
|
||||
|
||||
extension CXProvider: CXProviderProtocol { }
|
||||
@@ -14,6 +14,12 @@ import MatrixRustSDK
|
||||
import PushKit
|
||||
import UIKit
|
||||
|
||||
// Keep this class testable
|
||||
struct TimeProvider {
|
||||
var clock: any Clock<Duration>
|
||||
var now: () -> Date
|
||||
}
|
||||
|
||||
class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
|
||||
private struct CallID: Equatable {
|
||||
let callKitID: UUID
|
||||
@@ -23,20 +29,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
private let pushRegistry: PKPushRegistry
|
||||
private let callController = CXCallController()
|
||||
private let callProvider: CXProvider = {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.includesCallsInRecents = true
|
||||
|
||||
if let callKitIcon = UIImage(named: "images/app-logo") {
|
||||
configuration.iconTemplateImageData = callKitIcon.pngData()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/46077628/730924
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
|
||||
return CXProvider(configuration: configuration)
|
||||
}()
|
||||
private let callProvider: CXProviderProtocol
|
||||
private let timeProvider: TimeProvider
|
||||
|
||||
private weak var clientProxy: ClientProxyProtocol? {
|
||||
didSet {
|
||||
@@ -72,15 +66,34 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
private var declineListenerHandle: TaskHandle?
|
||||
|
||||
override init() {
|
||||
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil) {
|
||||
pushRegistry = PKPushRegistry(queue: nil)
|
||||
|
||||
self.timeProvider = timeProvider ?? TimeProvider(clock: ContinuousClock(), now: Date.init)
|
||||
|
||||
if let callProvider {
|
||||
self.callProvider = callProvider
|
||||
} else {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.includesCallsInRecents = true
|
||||
|
||||
if let callKitIcon = UIImage(named: "images/app-logo") {
|
||||
configuration.iconTemplateImageData = callKitIcon.pngData()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/46077628/730924
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
|
||||
self.callProvider = CXProvider(configuration: configuration)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
pushRegistry.delegate = self
|
||||
pushRegistry.desiredPushTypes = [.voIP]
|
||||
|
||||
callProvider.setDelegate(self, queue: nil)
|
||||
self.callProvider.setDelegate(self, queue: nil)
|
||||
}
|
||||
|
||||
func setClientProxy(_ clientProxy: any ClientProxyProtocol) {
|
||||
@@ -164,6 +177,20 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID)
|
||||
incomingCallID = callID
|
||||
|
||||
guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else {
|
||||
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
|
||||
return
|
||||
}
|
||||
|
||||
let nowDate = timeProvider.now()
|
||||
|
||||
guard nowDate < expirationDate else {
|
||||
MXLog.warning("Call expired for room \(roomID), ignoring incoming push")
|
||||
return
|
||||
}
|
||||
|
||||
let ringDuration: Duration = .seconds(min(expirationDate.timeIntervalSince1970 - nowDate.timeIntervalSince1970, 90))
|
||||
|
||||
let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
|
||||
|
||||
let update = CXCallUpdate()
|
||||
@@ -183,7 +210,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
}
|
||||
|
||||
endUnansweredCallTask = Task { [weak self] in
|
||||
try? await Task.sleep(for: .seconds(90))
|
||||
try? await self?.timeProvider.clock.sleep(for: ringDuration)
|
||||
|
||||
guard let self, !Task.isCancelled else {
|
||||
return
|
||||
|
||||
@@ -14,6 +14,8 @@ enum ElementCallServiceNotificationKey: String {
|
||||
/// When an incoming call is set to ring, there will be a `m.rtc.notification`event (MSC4075).
|
||||
/// Keep the notification event id as it is needed to decline calls (MSC4310).
|
||||
case rtcNotifyEventID
|
||||
/// The Date at which the incoming call should stop ringing.
|
||||
case expirationDate
|
||||
}
|
||||
|
||||
let ElementCallServiceNotificationDiscardDelta = 15.0
|
||||
|
||||
@@ -178,6 +178,14 @@
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Packages/swift-clocks</string>
|
||||
<key>Title</key>
|
||||
<string>swift-clocks</string>
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Packages/swift-collections</string>
|
||||
@@ -186,6 +194,14 @@
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Packages/swift-concurrency-extras</string>
|
||||
<key>Title</key>
|
||||
<string>swift-concurrency-extras</string>
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Packages/swift-custom-dump</string>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
|
||||
Copyright (c) 2022 Point-Free
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>MIT License
|
||||
|
||||
Copyright (c) 2023 Point-Free
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -125,10 +125,11 @@ class NotificationHandler {
|
||||
}
|
||||
|
||||
return .processedShouldDiscard
|
||||
case .rtcNotification(let notificationType, _):
|
||||
case .rtcNotification(let notificationType, let expirationTimestamp):
|
||||
return await handleCallNotification(notificationType: notificationType,
|
||||
rtcNotifyEventID: event.eventId(),
|
||||
timestamp: event.timestamp(),
|
||||
expirationTimestamp: expirationTimestamp,
|
||||
roomID: itemProxy.roomID,
|
||||
roomDisplayName: itemProxy.roomDisplayName)
|
||||
case .callAnswer,
|
||||
@@ -157,6 +158,7 @@ class NotificationHandler {
|
||||
private func handleCallNotification(notificationType: RtcNotificationType,
|
||||
rtcNotifyEventID: String,
|
||||
timestamp: Timestamp,
|
||||
expirationTimestamp: Timestamp,
|
||||
roomID: String,
|
||||
roomDisplayName: String) async -> NotificationProcessingResult {
|
||||
// Handle incoming VoIP calls, show the native OS call screen
|
||||
@@ -208,9 +210,11 @@ class NotificationHandler {
|
||||
}
|
||||
}
|
||||
|
||||
let expirationDate = Date(timeIntervalSince1970: TimeInterval(expirationTimestamp / 1000))
|
||||
let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID,
|
||||
ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName,
|
||||
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID]
|
||||
ElementCallServiceNotificationKey.expirationDate.rawValue: expirationDate,
|
||||
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID] as [String: Any]
|
||||
|
||||
do {
|
||||
try await CXProvider.reportNewIncomingVoIPPushPayload(payload)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import AnalyticsEvents
|
||||
import AVFoundation
|
||||
import CallKit
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
import Photos
|
||||
|
||||
131
UnitTests/Sources/ElementCallServiceTests.swift
Normal file
131
UnitTests/Sources/ElementCallServiceTests.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Clocks
|
||||
import PushKit
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class ElementCallServiceTests: XCTestCase {
|
||||
var callProvider: CXProviderMock!
|
||||
var currentDate: Date!
|
||||
var testClock: TestClock<Duration>!
|
||||
let pushRegistry = PKPushRegistry(queue: nil)
|
||||
|
||||
var service: ElementCallService!
|
||||
|
||||
func testIncomingCall() async {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Call accepted")
|
||||
|
||||
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
|
||||
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
XCTAssertTrue(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
}
|
||||
|
||||
func testCallIsTimingOut() async {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Call accepted")
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
|
||||
// advance past the timeout
|
||||
await testClock.advance(by: .seconds(30))
|
||||
|
||||
// Call should have ended with unanswered
|
||||
XCTAssertTrue(callProvider.reportCallWithEndedAtReasonCalled)
|
||||
XCTAssertEqual(callProvider.reportCallWithEndedAtReasonReceivedArguments?.reason, .unanswered)
|
||||
}
|
||||
|
||||
func testExpiredRingLifetimeIsIgnored() async {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
|
||||
|
||||
currentDate = currentDate.addingTimeInterval(60)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) { }
|
||||
sleep(20)
|
||||
|
||||
XCTAssertTrue(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
}
|
||||
|
||||
func testLifetimeIsCapped() async {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) { }
|
||||
|
||||
// advance pass the max timeout but below the 300
|
||||
await testClock.advance(by: .seconds(100))
|
||||
|
||||
// Call should have ended with unanswered
|
||||
XCTAssertTrue(callProvider.reportCallWithEndedAtReasonCalled)
|
||||
XCTAssertEqual(callProvider.reportCallWithEndedAtReasonReceivedArguments?.reason, .unanswered)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupService() {
|
||||
callProvider = CXProviderMock(.init())
|
||||
currentDate = Date()
|
||||
testClock = TestClock()
|
||||
let dateProvider: () -> Date = {
|
||||
self.currentDate
|
||||
}
|
||||
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: testClock, now: dateProvider))
|
||||
}
|
||||
}
|
||||
|
||||
private class PKPushPayloadMock: PKPushPayload {
|
||||
var dict: [AnyHashable: Any] = [:]
|
||||
|
||||
override init() {
|
||||
dict[ElementCallServiceNotificationKey.roomID.rawValue] = "!room:example.com"
|
||||
dict[ElementCallServiceNotificationKey.roomDisplayName.rawValue] = "welcome"
|
||||
dict[ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue] = "$000"
|
||||
dict[ElementCallServiceNotificationKey.expirationDate.rawValue] = Date(timeIntervalSince1970: 10)
|
||||
}
|
||||
|
||||
override var dictionaryPayload: [AnyHashable: Any] {
|
||||
dict
|
||||
}
|
||||
|
||||
func updatingExpiration(_ from: Date, lifetime: TimeInterval) -> Self {
|
||||
dict[ElementCallServiceNotificationKey.expirationDate.rawValue] = from.addingTimeInterval(lifetime)
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ targets:
|
||||
- target: ElementX
|
||||
- package: MatrixRustSDK
|
||||
- package: AsyncAlgorithms
|
||||
- package: Clocks
|
||||
|
||||
info:
|
||||
path: ../SupportingFiles/Info.plist
|
||||
|
||||
@@ -102,6 +102,9 @@ packages:
|
||||
AsyncAlgorithms:
|
||||
url: https://github.com/apple/swift-async-algorithms
|
||||
minorVersion: 1.0.0
|
||||
Clocks:
|
||||
url: https://github.com/pointfreeco/swift-clocks
|
||||
from: 1.0.6
|
||||
Collections:
|
||||
url: https://github.com/apple/swift-collections
|
||||
minorVersion: 1.2.0
|
||||
|
||||
Reference in New Issue
Block a user