Live Location Sharing - handle server echoes (#5514)
* Track active live location sessions by ID instead of timeout. # Conflicts: # ElementX/Sources/Services/Location/LiveLocationManager.swift * implemented a system to promote starting session to active sesessions to send locations at the right time, and a system to remove a local session if it's handled by an external device. * pr suggestions --------- Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -835,6 +835,7 @@
|
|||||||
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; };
|
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; };
|
||||||
8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
|
8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
|
||||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; };
|
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; };
|
||||||
|
8AEE47A14223423806A7653A /* LiveLocationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */; };
|
||||||
8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260004737C573A56FA01E86E /* Encodable.swift */; };
|
8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260004737C573A56FA01E86E /* Encodable.swift */; };
|
||||||
8B408C574E35E1C9B43A50CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */; };
|
8B408C574E35E1C9B43A50CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */; };
|
||||||
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; };
|
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; };
|
||||||
@@ -963,6 +964,7 @@
|
|||||||
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
||||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
||||||
A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; };
|
A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; };
|
||||||
|
A0646F23876D6326AD27FF52 /* LiveLocationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */; };
|
||||||
A07178337F3C0B208B5A77A8 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6ED50FE104992419310EEB /* NotificationHandler.swift */; };
|
A07178337F3C0B208B5A77A8 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6ED50FE104992419310EEB /* NotificationHandler.swift */; };
|
||||||
A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */; };
|
A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63072981793CCA84EE12798 /* ChatsSpaceFiltersScreenViewModelProtocol.swift */; };
|
||||||
A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */; };
|
A0861B727B273B5B3DD7FBF6 /* KnockRequestsListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09227E671DB30795C43FFFD /* KnockRequestsListScreenViewModel.swift */; };
|
||||||
@@ -1266,6 +1268,7 @@
|
|||||||
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; };
|
D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; };
|
||||||
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; };
|
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; };
|
||||||
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; };
|
D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; };
|
||||||
|
D3ED5692672892F9F1E7375A /* LiveLocationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */; };
|
||||||
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
|
D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; };
|
||||||
D433A58BFF77B3E563FB547E /* RoomCallControlsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */; };
|
D433A58BFF77B3E563FB547E /* RoomCallControlsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */; };
|
||||||
D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; };
|
D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; };
|
||||||
@@ -2843,6 +2846,7 @@
|
|||||||
D97A4E73EA97CA08D2BB9806 /* RoomScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenTests.swift; sourceTree = "<group>"; };
|
D97A4E73EA97CA08D2BB9806 /* RoomScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenTests.swift; sourceTree = "<group>"; };
|
||||||
D9C5AA3EF7EC67C01C75CEDD /* LabsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreen.swift; sourceTree = "<group>"; };
|
D9C5AA3EF7EC67C01C75CEDD /* LabsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreen.swift; sourceTree = "<group>"; };
|
||||||
DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
|
DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
|
||||||
|
DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationSession.swift; sourceTree = "<group>"; };
|
||||||
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerProtocol.swift; sourceTree = "<group>"; };
|
DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||||
DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; };
|
DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; };
|
||||||
@@ -6513,6 +6517,7 @@
|
|||||||
7BD5523BDEDB247E29228476 /* AppSettings.swift */,
|
7BD5523BDEDB247E29228476 /* AppSettings.swift */,
|
||||||
F78B4E56DBFFD4A7A39D10F5 /* AudioPlaybackSpeed.swift */,
|
F78B4E56DBFFD4A7A39D10F5 /* AudioPlaybackSpeed.swift */,
|
||||||
0A2074C0449B83D5858BD2D7 /* FrequentlyUsedEmoji.swift */,
|
0A2074C0449B83D5858BD2D7 /* FrequentlyUsedEmoji.swift */,
|
||||||
|
DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */,
|
||||||
8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */,
|
8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */,
|
||||||
E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */,
|
E8D354D4232DED9649FD0FF4 /* OIDCConfiguration.swift */,
|
||||||
8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */,
|
8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */,
|
||||||
@@ -7778,6 +7783,7 @@
|
|||||||
8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */,
|
8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */,
|
||||||
A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */,
|
A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */,
|
||||||
FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */,
|
FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */,
|
||||||
|
D3ED5692672892F9F1E7375A /* LiveLocationSession.swift in Sources */,
|
||||||
0728314DD51AC3819F818EA8 /* LogLevel.swift in Sources */,
|
0728314DD51AC3819F818EA8 /* LogLevel.swift in Sources */,
|
||||||
AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */,
|
AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */,
|
||||||
9AC47275B8E1EB0976BA7A80 /* MapTilerConfiguration.swift in Sources */,
|
9AC47275B8E1EB0976BA7A80 /* MapTilerConfiguration.swift in Sources */,
|
||||||
@@ -8009,6 +8015,7 @@
|
|||||||
05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */,
|
05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */,
|
||||||
FC31493979ED1FDF7D5EA3F9 /* KeychainController.swift in Sources */,
|
FC31493979ED1FDF7D5EA3F9 /* KeychainController.swift in Sources */,
|
||||||
5618ED25F092DF5712003829 /* KeychainControllerProtocol.swift in Sources */,
|
5618ED25F092DF5712003829 /* KeychainControllerProtocol.swift in Sources */,
|
||||||
|
A0646F23876D6326AD27FF52 /* LiveLocationSession.swift in Sources */,
|
||||||
DA10C99BA43A0F1E732F6274 /* LogLevel.swift in Sources */,
|
DA10C99BA43A0F1E732F6274 /* LogLevel.swift in Sources */,
|
||||||
0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */,
|
0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */,
|
||||||
074F741578307EF0179EE47C /* MapTilerConfiguration.swift in Sources */,
|
074F741578307EF0179EE47C /* MapTilerConfiguration.swift in Sources */,
|
||||||
@@ -8467,6 +8474,7 @@
|
|||||||
CD077E14FAADC444C5A80068 /* LiveLocationManagerProtocol.swift in Sources */,
|
CD077E14FAADC444C5A80068 /* LiveLocationManagerProtocol.swift in Sources */,
|
||||||
C8D0AC22E03F652118A2BB73 /* LiveLocationRoomTimelineItem.swift in Sources */,
|
C8D0AC22E03F652118A2BB73 /* LiveLocationRoomTimelineItem.swift in Sources */,
|
||||||
F7977C53B2B1D73030C69761 /* LiveLocationRoomTimelineView.swift in Sources */,
|
F7977C53B2B1D73030C69761 /* LiveLocationRoomTimelineView.swift in Sources */,
|
||||||
|
8AEE47A14223423806A7653A /* LiveLocationSession.swift in Sources */,
|
||||||
633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */,
|
633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */,
|
||||||
9223E5F2A2CE0AFFDFF0AFFB /* LiveLocationSharingBannerView.swift in Sources */,
|
9223E5F2A2CE0AFFDFF0AFFB /* LiveLocationSharingBannerView.swift in Sources */,
|
||||||
EB5B79DD2BCAF8F3B8B01F2F /* LiveLocationSheet.swift in Sources */,
|
EB5B79DD2BCAF8F3B8B01F2F /* LiveLocationSheet.swift in Sources */,
|
||||||
|
|||||||
@@ -352,8 +352,8 @@ final class AppSettings {
|
|||||||
|
|
||||||
// MARK: - Live Location
|
// MARK: - Live Location
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.liveLocationSharingTimeoutDatesByRoomID, defaultValue: [String: Date](), storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.liveLocationSharingTimeoutDatesByRoomID, defaultValue: [String: LiveLocationSession](), storageType: .userDefaults(store))
|
||||||
var liveLocationSharingTimeoutDatesByRoomID
|
var liveLocationSharingSessionsByRoomID
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.liveLocationMinimumDistanceUpdate, defaultValue: 10, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.liveLocationMinimumDistanceUpdate, defaultValue: 10, storageType: .userDefaults(store))
|
||||||
var liveLocationMinimumDistanceUpdate
|
var liveLocationMinimumDistanceUpdate
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct LiveLocationSession: Codable, Equatable {
|
||||||
|
let eventID: String
|
||||||
|
let expirationDate: Date
|
||||||
|
}
|
||||||
@@ -159,6 +159,8 @@ extension ClientProxyMock {
|
|||||||
|
|
||||||
underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject<TimelineMediaVisibility, Never>(configuration.timelineMediaVisibility).asCurrentValuePublisher()
|
underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject<TimelineMediaVisibility, Never>(configuration.timelineMediaVisibility).asCurrentValuePublisher()
|
||||||
underlyingHideInviteAvatarsPublisher = CurrentValueSubject<Bool, Never>(configuration.hideInviteAvatars).asCurrentValuePublisher()
|
underlyingHideInviteAvatarsPublisher = CurrentValueSubject<Bool, Never>(configuration.hideInviteAvatars).asCurrentValuePublisher()
|
||||||
|
|
||||||
|
liveLocationOwnInfoUpdatesPublisher = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>().eraseToAnyPublisher()
|
||||||
|
|
||||||
underlyingMaxMediaUploadSize = .success(configuration.maxMediaUploadSize)
|
underlyingMaxMediaUploadSize = .success(configuration.maxMediaUploadSize)
|
||||||
|
|
||||||
|
|||||||
@@ -2854,6 +2854,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
var underlyingMaxMediaUploadSize: Result<UInt, ClientProxyError>!
|
var underlyingMaxMediaUploadSize: Result<UInt, ClientProxyError>!
|
||||||
var maxMediaUploadSizeClosure: (() async -> Result<UInt, ClientProxyError>)?
|
var maxMediaUploadSizeClosure: (() async -> Result<UInt, ClientProxyError>)?
|
||||||
|
var liveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never> {
|
||||||
|
get { return underlyingLiveLocationOwnInfoUpdatesPublisher }
|
||||||
|
set(value) { underlyingLiveLocationOwnInfoUpdatesPublisher = value }
|
||||||
|
}
|
||||||
|
var underlyingLiveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never>!
|
||||||
|
|
||||||
//MARK: - isOnlyDeviceLeft
|
//MARK: - isOnlyDeviceLeft
|
||||||
|
|
||||||
@@ -10766,13 +10771,13 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
var startLiveLocationShareDurationReceivedDuration: Duration?
|
var startLiveLocationShareDurationReceivedDuration: Duration?
|
||||||
var startLiveLocationShareDurationReceivedInvocations: [Duration] = []
|
var startLiveLocationShareDurationReceivedInvocations: [Duration] = []
|
||||||
|
|
||||||
var startLiveLocationShareDurationUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
var startLiveLocationShareDurationUnderlyingReturnValue: Result<String, RoomProxyError>!
|
||||||
var startLiveLocationShareDurationReturnValue: Result<Void, RoomProxyError>! {
|
var startLiveLocationShareDurationReturnValue: Result<String, RoomProxyError>! {
|
||||||
get {
|
get {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
return startLiveLocationShareDurationUnderlyingReturnValue
|
return startLiveLocationShareDurationUnderlyingReturnValue
|
||||||
} else {
|
} else {
|
||||||
var returnValue: Result<Void, RoomProxyError>? = nil
|
var returnValue: Result<String, RoomProxyError>? = nil
|
||||||
DispatchQueue.main.sync {
|
DispatchQueue.main.sync {
|
||||||
returnValue = startLiveLocationShareDurationUnderlyingReturnValue
|
returnValue = startLiveLocationShareDurationUnderlyingReturnValue
|
||||||
}
|
}
|
||||||
@@ -10790,9 +10795,9 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var startLiveLocationShareDurationClosure: ((Duration) async -> Result<Void, RoomProxyError>)?
|
var startLiveLocationShareDurationClosure: ((Duration) async -> Result<String, RoomProxyError>)?
|
||||||
|
|
||||||
func startLiveLocationShare(duration: Duration) async -> Result<Void, RoomProxyError> {
|
func startLiveLocationShare(duration: Duration) async -> Result<String, RoomProxyError> {
|
||||||
startLiveLocationShareDurationCallsCount += 1
|
startLiveLocationShareDurationCallsCount += 1
|
||||||
startLiveLocationShareDurationReceivedDuration = duration
|
startLiveLocationShareDurationReceivedDuration = duration
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
@@ -203,6 +203,12 @@ extension SDKListener: LiveLocationsListener where T == [LiveLocationShareUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SDKListener: BeaconInfoListener where T == BeaconInfoUpdate {
|
||||||
|
func onUpdate(update: BeaconInfoUpdate) {
|
||||||
|
onUpdateClosure(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension SDKListener: ThreadListEntriesListener where T == [ThreadListUpdate] {
|
extension SDKListener: ThreadListEntriesListener where T == [ThreadListUpdate] {
|
||||||
func onUpdate(diff: [ThreadListUpdate]) {
|
func onUpdate(diff: [ThreadListUpdate]) {
|
||||||
onUpdateClosure(diff)
|
onUpdateClosure(diff)
|
||||||
|
|||||||
@@ -171,11 +171,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationSharingTimeoutDatesByRoomID
|
appSettings.$liveLocationSharingSessionsByRoomID
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] timeoutDatesByRoomID in
|
.sink { [weak self] sessionsByRoomID in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
state.isSharingLiveLocation = timeoutDatesByRoomID.keys.contains(roomProxy.id)
|
state.isSharingLiveLocation = sessionsByRoomID.keys.contains(roomProxy.id)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
// periphery:ignore - required for instance retention in the rust codebase
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
|
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
|
||||||
|
|
||||||
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
|
private var liveLocationOwnInfoUpdatesListenerTaskHandle: TaskHandle?
|
||||||
|
|
||||||
private var delegateHandle: TaskHandle?
|
private var delegateHandle: TaskHandle?
|
||||||
|
|
||||||
@@ -188,7 +191,12 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var roomsToAwait: Set<String> = []
|
var roomsToAwait: Set<String> = []
|
||||||
|
|
||||||
|
private let liveLocationOwnInfoUpdatesSubject = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>()
|
||||||
|
var liveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never> {
|
||||||
|
liveLocationOwnInfoUpdatesSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
private let sendQueueStatusSubject = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
init(client: ClientProtocol,
|
init(client: ClientProtocol,
|
||||||
@@ -270,6 +278,8 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
Task {
|
Task {
|
||||||
mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver()
|
mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveLocationOwnInfoUpdatesListenerTaskHandle = createLiveLocationOwnInfoUpdatesObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
var userID: String {
|
var userID: String {
|
||||||
@@ -1136,6 +1146,21 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createLiveLocationOwnInfoUpdatesObserver() -> TaskHandle? {
|
||||||
|
do {
|
||||||
|
return try client.subscribeToOwnBeaconInfoUpdates(listener: SDKListener { [weak self] update in
|
||||||
|
guard let self else { return }
|
||||||
|
let appUpdate = LiveLocationOwnInfoUpdate(roomID: update.roomId,
|
||||||
|
eventID: update.eventId,
|
||||||
|
isLive: update.live)
|
||||||
|
liveLocationOwnInfoUpdatesSubject.send(appUpdate)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed creating own beacon info updates observer: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle {
|
||||||
roomListService.state(listener: SDKListener { [weak self] state in
|
roomListService.state(listener: SDKListener { [weak self] state in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ enum TimelineMediaVisibility: Decodable {
|
|||||||
case never
|
case never
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a server-echoed update about the current user's own beacon info state in a room.
|
||||||
|
struct LiveLocationOwnInfoUpdate: Equatable {
|
||||||
|
/// The room where the beacon info event was sent.
|
||||||
|
let roomID: String
|
||||||
|
/// The event ID of the beacon info state event.
|
||||||
|
let eventID: String
|
||||||
|
/// Whether the beacon is currently active (live) or has been stopped.
|
||||||
|
let isLive: Bool
|
||||||
|
}
|
||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol ClientProxyProtocol: AnyObject {
|
protocol ClientProxyProtocol: AnyObject {
|
||||||
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
var actionsPublisher: AnyPublisher<ClientProxyAction, Never> { get }
|
||||||
@@ -264,6 +274,11 @@ protocol ClientProxyProtocol: AnyObject {
|
|||||||
|
|
||||||
func userIdentity(for userID: String, fallBackToServer: Bool) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
|
func userIdentity(for userID: String, fallBackToServer: Bool) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
|
||||||
|
|
||||||
|
// MARK: - Live Location
|
||||||
|
|
||||||
|
/// Publishes updates about the current user's own live location beacon info state changes (start/stop) as echoed by the server.
|
||||||
|
var liveLocationOwnInfoUpdatesPublisher: AnyPublisher<LiveLocationOwnInfoUpdate, Never> { get }
|
||||||
|
|
||||||
// MARK: - Moderation & Safety
|
// MARK: - Moderation & Safety
|
||||||
|
|
||||||
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError>
|
func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result<Void, ClientProxyError>
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
/// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary.
|
/// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary.
|
||||||
private var activeRoomProxies = [String: JoinedRoomProxyProtocol]()
|
private var activeRoomProxies = [String: JoinedRoomProxyProtocol]()
|
||||||
|
|
||||||
|
/// Sessions that have been requested but not yet confirmed by the server echo.
|
||||||
|
/// Once the server acknowledges the beacon info, sessions are promoted to the persistent store.
|
||||||
|
private var startingLiveLocationSharingSessionsByRoomID = [String: LiveLocationSession]()
|
||||||
|
|
||||||
/// Subject used to pipe location updates into the backpressure-aware processing loop.
|
/// Subject used to pipe location updates into the backpressure-aware processing loop.
|
||||||
private let locationUpdateSubject = PassthroughSubject<CLLocationCoordinate2D, Never>()
|
private let locationUpdateSubject = PassthroughSubject<CLLocationCoordinate2D, Never>()
|
||||||
|
|
||||||
/// The most recent location update waiting to be sent. When a send is already in progress,
|
/// The most recent location update waiting to be sent. When a send is already in progress,
|
||||||
/// new updates overwrite this value so only the latest is sent once the current send completes.
|
/// new updates overwrite this value so only the latest is sent once the current send completes.
|
||||||
private var latestPendingLocation: CLLocationCoordinate2D?
|
private var latestPendingLocation: CLLocationCoordinate2D?
|
||||||
|
|
||||||
/// Whether a location send cycle (send + minimum delay) is currently in progress.
|
/// Whether a location send cycle (send + minimum delay) is currently in progress.
|
||||||
private var isProcessingLocationUpdate = false
|
private var isProcessingLocationUpdate = false
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private var isUpdatingLocation = false
|
private var isUpdatingLocation = false
|
||||||
@@ -64,7 +68,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: appSettings.liveLocationMinimumDistanceUpdate)
|
setupMinimumDistanceUpdatesAndAccuracy(minimumDistance: appSettings.liveLocationMinimumDistanceUpdate)
|
||||||
setupSubscriptions()
|
setupSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - LiveLocationManagerProtocol
|
// MARK: - LiveLocationManagerProtocol
|
||||||
|
|
||||||
var hasDisplayedLiveLocationDisclaimer: Bool {
|
var hasDisplayedLiveLocationDisclaimer: Bool {
|
||||||
@@ -87,7 +91,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
func startLiveLocation(roomID: String, duration: Duration) async -> Result<Void, LiveLocationManagerError> {
|
func startLiveLocation(roomID: String, duration: Duration) async -> Result<Void, LiveLocationManagerError> {
|
||||||
// Stop any existing session for this room first
|
// Stop any existing session for this room first
|
||||||
var didAlreadyStopLocalSession = false
|
var didAlreadyStopLocalSession = false
|
||||||
if appSettings.liveLocationSharingTimeoutDatesByRoomID[roomID] != nil {
|
if appSettings.liveLocationSharingSessionsByRoomID[roomID] != nil
|
||||||
|
|| startingLiveLocationSharingSessionsByRoomID[roomID] != nil {
|
||||||
await stopLiveLocation(roomID: roomID)
|
await stopLiveLocation(roomID: roomID)
|
||||||
didAlreadyStopLocalSession = true
|
didAlreadyStopLocalSession = true
|
||||||
}
|
}
|
||||||
@@ -104,20 +109,13 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
}
|
}
|
||||||
let result = await roomProxy.startLiveLocationShare(duration: duration)
|
let result = await roomProxy.startLiveLocationShare(duration: duration)
|
||||||
|
|
||||||
guard case .success = result else {
|
guard case .success(let eventID) = result else {
|
||||||
MXLog.error("Failed to start live location share in room: \(roomID)")
|
MXLog.error("Failed to start live location share in room: \(roomID)")
|
||||||
return .failure(.startFailed)
|
return .failure(.startFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutDate = Date().addingTimeInterval(TimeInterval(duration.seconds))
|
let expirationDate = Date().addingTimeInterval(TimeInterval(duration.seconds))
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID[roomID] = timeoutDate
|
startingLiveLocationSharingSessionsByRoomID[roomID] = LiveLocationSession(eventID: eventID, expirationDate: expirationDate)
|
||||||
|
|
||||||
if isUpdatingLocation, let lastLocation {
|
|
||||||
// To make sure the newly started session is in sync with the existing ones,
|
|
||||||
// we re-send the last location received by the manager.
|
|
||||||
// Otherwise we would need to wait a distance filtered update.
|
|
||||||
locationUpdateSubject.send(lastLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
@@ -125,7 +123,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
func stopLiveLocation(roomID: String) async {
|
func stopLiveLocation(roomID: String) async {
|
||||||
var roomProxy: JoinedRoomProxyProtocol?
|
var roomProxy: JoinedRoomProxyProtocol?
|
||||||
let cachedRoomProxy = activeRoomProxies[roomID]
|
let cachedRoomProxy = activeRoomProxies[roomID]
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeValue(forKey: roomID)
|
startingLiveLocationSharingSessionsByRoomID.removeValue(forKey: roomID)
|
||||||
|
appSettings.liveLocationSharingSessionsByRoomID.removeValue(forKey: roomID)
|
||||||
|
|
||||||
if let cachedRoomProxy {
|
if let cachedRoomProxy {
|
||||||
roomProxy = cachedRoomProxy
|
roomProxy = cachedRoomProxy
|
||||||
@@ -186,12 +185,20 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
appSettings.$liveLocationSharingTimeoutDatesByRoomID
|
clientProxy.liveLocationOwnInfoUpdatesPublisher
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] update in
|
||||||
|
guard let self else { return }
|
||||||
|
handleBeaconInfoUpdate(update)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
appSettings.$liveLocationSharingSessionsByRoomID
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { [weak self] sessions in
|
.sink { [weak self] sessions in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
syncActiveRoomProxies(with: sessions)
|
syncActiveRoomProxies(with: sessions)
|
||||||
|
|
||||||
if sessions.isEmpty {
|
if sessions.isEmpty {
|
||||||
self.stopUpdatingLocation()
|
self.stopUpdatingLocation()
|
||||||
} else {
|
} else {
|
||||||
@@ -209,7 +216,32 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncActiveRoomProxies(with sessions: [String: Date]) {
|
private func handleBeaconInfoUpdate(_ update: LiveLocationOwnInfoUpdate) {
|
||||||
|
// A new beaconInfo has been received in a room with existing active session.
|
||||||
|
// This is either a new start or a new stop from a different device, so we
|
||||||
|
// should remove the session from the current local one.
|
||||||
|
appSettings.liveLocationSharingSessionsByRoomID.removeValue(forKey: update.roomID)
|
||||||
|
|
||||||
|
// Instead if we receive a new isLiveUpdate
|
||||||
|
guard update.isLive else { return }
|
||||||
|
|
||||||
|
// That belongs to a session that is starting in a room and matches the eventID
|
||||||
|
guard let session = startingLiveLocationSharingSessionsByRoomID[update.roomID],
|
||||||
|
session.eventID == update.eventID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This means the server has echoed the start of the session and we can safely promote it
|
||||||
|
// to a started session and start sending live locations.
|
||||||
|
startingLiveLocationSharingSessionsByRoomID.removeValue(forKey: update.roomID)
|
||||||
|
appSettings.liveLocationSharingSessionsByRoomID[update.roomID] = session
|
||||||
|
|
||||||
|
if isUpdatingLocation, let lastLocation {
|
||||||
|
locationUpdateSubject.send(lastLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncActiveRoomProxies(with sessions: [String: LiveLocationSession]) {
|
||||||
// Remove proxies for rooms no longer in the dictionary.
|
// Remove proxies for rooms no longer in the dictionary.
|
||||||
let activeRoomIDs = Set(sessions.keys)
|
let activeRoomIDs = Set(sessions.keys)
|
||||||
for roomID in activeRoomProxies.keys where !activeRoomIDs.contains(roomID) {
|
for roomID in activeRoomProxies.keys where !activeRoomIDs.contains(roomID) {
|
||||||
@@ -259,11 +291,11 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
/// discarding any intermediate updates while always keeping the last one.
|
/// discarding any intermediate updates while always keeping the last one.
|
||||||
private func processLocationUpdateIfNeeded() {
|
private func processLocationUpdateIfNeeded() {
|
||||||
guard !isProcessingLocationUpdate, let location = latestPendingLocation else { return }
|
guard !isProcessingLocationUpdate, let location = latestPendingLocation else { return }
|
||||||
guard !appSettings.liveLocationSharingTimeoutDatesByRoomID.isEmpty else { return }
|
guard !appSettings.liveLocationSharingSessionsByRoomID.isEmpty else { return }
|
||||||
|
|
||||||
latestPendingLocation = nil
|
latestPendingLocation = nil
|
||||||
isProcessingLocationUpdate = true
|
isProcessingLocationUpdate = true
|
||||||
|
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
@@ -282,13 +314,13 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
processLocationUpdateIfNeeded()
|
processLocationUpdateIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async {
|
private func sendLocationToActiveRooms(_ coordinate: CLLocationCoordinate2D) async {
|
||||||
let sessions = appSettings.liveLocationSharingTimeoutDatesByRoomID
|
let sessions = appSettings.liveLocationSharingSessionsByRoomID
|
||||||
let geoURI = GeoURI(coordinate: coordinate, uncertainty: nil)
|
let geoURI = GeoURI(coordinate: coordinate, uncertainty: nil)
|
||||||
|
|
||||||
for (roomID, timeoutDate) in sessions {
|
for (roomID, session) in sessions {
|
||||||
if Date() >= timeoutDate {
|
if Date() >= session.expirationDate {
|
||||||
MXLog.info("Live location session expired for room: \(roomID)")
|
MXLog.info("Live location session expired for room: \(roomID)")
|
||||||
await stopLiveLocation(roomID: roomID)
|
await stopLiveLocation(roomID: roomID)
|
||||||
continue
|
continue
|
||||||
@@ -306,7 +338,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
switch error {
|
switch error {
|
||||||
case .liveLocationSessionIsNotActive:
|
case .liveLocationSessionIsNotActive:
|
||||||
MXLog.error("Failed to send live locatio update to room \(roomID): session not active")
|
MXLog.error("Failed to send live location update to room \(roomID): session not active")
|
||||||
await stopLiveLocation(roomID: roomID)
|
await stopLiveLocation(roomID: roomID)
|
||||||
default:
|
default:
|
||||||
MXLog.error("Failed to send live location update to room \(roomID): \(error)")
|
MXLog.error("Failed to send live location update to room \(roomID): \(error)")
|
||||||
@@ -329,7 +361,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func stopAllSessions() {
|
private func stopAllSessions() {
|
||||||
let roomIDs = Array(appSettings.liveLocationSharingTimeoutDatesByRoomID.keys)
|
let roomIDs = Array(Set(appSettings.liveLocationSharingSessionsByRoomID.keys)
|
||||||
|
.union(startingLiveLocationSharingSessionsByRoomID.keys))
|
||||||
Task { [weak self] in
|
Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
for roomID in roomIDs {
|
for roomID in roomIDs {
|
||||||
|
|||||||
@@ -758,10 +758,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
await RoomLiveLocationService(liveLocationsObserver: room.liveLocationsObserver())
|
await RoomLiveLocationService(liveLocationsObserver: room.liveLocationsObserver())
|
||||||
}
|
}
|
||||||
|
|
||||||
func startLiveLocationShare(duration: Duration) async -> Result<Void, RoomProxyError> {
|
func startLiveLocationShare(duration: Duration) async -> Result<String, RoomProxyError> {
|
||||||
do {
|
do {
|
||||||
try await room.startLiveLocationShare(durationMillis: UInt64(duration.seconds * 1000))
|
let eventID = try await room.startLiveLocationShare(durationMillis: UInt64(duration.seconds * 1000))
|
||||||
return .success(())
|
return .success(eventID)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed starting live location share with error: \(error)")
|
MXLog.error("Failed starting live location share with error: \(error)")
|
||||||
return .failure(.sdkError(error))
|
return .failure(.sdkError(error))
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
|||||||
|
|
||||||
func makeLiveLocationService() async -> RoomLiveLocationServiceProtocol
|
func makeLiveLocationService() async -> RoomLiveLocationServiceProtocol
|
||||||
|
|
||||||
func startLiveLocationShare(duration: Duration) async -> Result<Void, RoomProxyError>
|
func startLiveLocationShare(duration: Duration) async -> Result<String, RoomProxyError>
|
||||||
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError>
|
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError>
|
||||||
func stopLiveLocationShare() async -> Result<Void, RoomProxyError>
|
func stopLiveLocationShare() async -> Result<Void, RoomProxyError>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -16,6 +17,7 @@ final class LiveLocationManagerTests {
|
|||||||
private var locationManagerMock: CLLocationManagerMock!
|
private var locationManagerMock: CLLocationManagerMock!
|
||||||
private var manager: LiveLocationManager!
|
private var manager: LiveLocationManager!
|
||||||
private var appSettings: AppSettings!
|
private var appSettings: AppSettings!
|
||||||
|
private var beaconInfoSubject: PassthroughSubject<LiveLocationOwnInfoUpdate, Never>!
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
AppSettings.resetAllSettings()
|
AppSettings.resetAllSettings()
|
||||||
@@ -40,14 +42,18 @@ final class LiveLocationManagerTests {
|
|||||||
}
|
}
|
||||||
roomProxy.startLiveLocationShareDurationClosure = { _ in
|
roomProxy.startLiveLocationShareDurationClosure = { _ in
|
||||||
callOrder.append("start")
|
callOrder.append("start")
|
||||||
return .success(())
|
return .success("$event:matrix.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||||
|
|
||||||
try result.get()
|
try result.get()
|
||||||
#expect(callOrder == ["stop", "start"])
|
#expect(callOrder == ["stop", "start"])
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
|
||||||
|
|
||||||
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
|
|
||||||
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
|
||||||
#expect(locationManagerMock.startUpdatingLocationCalled)
|
#expect(locationManagerMock.startUpdatingLocationCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ final class LiveLocationManagerTests {
|
|||||||
setUp()
|
setUp()
|
||||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
|
appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] = LiveLocationSession(eventID: "$old_event:matrix.org", expirationDate: Date().addingTimeInterval(300))
|
||||||
|
|
||||||
var callOrder: [String] = []
|
var callOrder: [String] = []
|
||||||
roomProxy.stopLiveLocationShareClosure = {
|
roomProxy.stopLiveLocationShareClosure = {
|
||||||
@@ -65,14 +71,17 @@ final class LiveLocationManagerTests {
|
|||||||
}
|
}
|
||||||
roomProxy.startLiveLocationShareDurationClosure = { _ in
|
roomProxy.startLiveLocationShareDurationClosure = { _ in
|
||||||
callOrder.append("start")
|
callOrder.append("start")
|
||||||
return .success(())
|
return .success("$event:matrix.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(600))
|
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(600))
|
||||||
|
|
||||||
try result.get()
|
try result.get()
|
||||||
#expect(callOrder == ["stop", "start"])
|
#expect(callOrder == ["stop", "start"])
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
|
|
||||||
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
|
|
||||||
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -81,12 +90,12 @@ final class LiveLocationManagerTests {
|
|||||||
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
||||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||||
|
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
|
appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
|
||||||
|
|
||||||
_ = await manager.startLiveLocation(roomID: "!room1:matrix.org", duration: .seconds(300))
|
_ = await manager.startLiveLocation(roomID: "!room1:matrix.org", duration: .seconds(300))
|
||||||
|
|
||||||
#expect(roomProxy.stopLiveLocationShareCalled)
|
#expect(roomProxy.stopLiveLocationShareCalled)
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -97,7 +106,7 @@ final class LiveLocationManagerTests {
|
|||||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||||
|
|
||||||
#expect(throws: LiveLocationManagerError.roomNotJoined) { try result.get() }
|
#expect(throws: LiveLocationManagerError.roomNotJoined) { try result.get() }
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -110,7 +119,7 @@ final class LiveLocationManagerTests {
|
|||||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||||
|
|
||||||
#expect(throws: LiveLocationManagerError.startFailed) { try result.get() }
|
#expect(throws: LiveLocationManagerError.startFailed) { try result.get() }
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -124,11 +133,14 @@ final class LiveLocationManagerTests {
|
|||||||
_ = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: duration)
|
_ = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: duration)
|
||||||
let afterStart = Date()
|
let afterStart = Date()
|
||||||
|
|
||||||
let storedTimeout = appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"]
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
|
|
||||||
|
let storedSession = try #require(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"])
|
||||||
let expectedMinTimeout = beforeStart.addingTimeInterval(TimeInterval(duration.seconds))
|
let expectedMinTimeout = beforeStart.addingTimeInterval(TimeInterval(duration.seconds))
|
||||||
let expectedMaxTimeout = afterStart.addingTimeInterval(TimeInterval(duration.seconds))
|
let expectedMaxTimeout = afterStart.addingTimeInterval(TimeInterval(duration.seconds))
|
||||||
|
|
||||||
try #expect((expectedMinTimeout...expectedMaxTimeout).contains(#require(storedTimeout)))
|
#expect((expectedMinTimeout...expectedMaxTimeout).contains(storedSession.expirationDate))
|
||||||
|
#expect(storedSession.eventID == "$event:matrix.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - stopLiveLocation
|
// MARK: - stopLiveLocation
|
||||||
@@ -138,12 +150,12 @@ final class LiveLocationManagerTests {
|
|||||||
setUp()
|
setUp()
|
||||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
|
appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
|
||||||
|
|
||||||
await manager.stopLiveLocation(roomID: "!room:matrix.org")
|
await manager.stopLiveLocation(roomID: "!room:matrix.org")
|
||||||
|
|
||||||
#expect(roomProxy.stopLiveLocationShareCalled)
|
#expect(roomProxy.stopLiveLocationShareCalled)
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
|
||||||
// Setting the timeout date above starts tracking; removing it stops tracking.
|
// Setting the timeout date above starts tracking; removing it stops tracking.
|
||||||
#expect(locationManagerMock.startUpdatingLocationCalled)
|
#expect(locationManagerMock.startUpdatingLocationCalled)
|
||||||
#expect(locationManagerMock.stopUpdatingLocationCalled)
|
#expect(locationManagerMock.stopUpdatingLocationCalled)
|
||||||
@@ -165,15 +177,34 @@ final class LiveLocationManagerTests {
|
|||||||
setUp()
|
setUp()
|
||||||
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
||||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] = Date().addingTimeInterval(300)
|
appSettings.liveLocationSharingSessionsByRoomID["!room1:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
|
||||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
|
appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] = LiveLocationSession(eventID: "$event:matrix.org", expirationDate: Date().addingTimeInterval(300))
|
||||||
|
|
||||||
await manager.stopLiveLocation(roomID: "!room1:matrix.org")
|
await manager.stopLiveLocation(roomID: "!room1:matrix.org")
|
||||||
|
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] == nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room1:matrix.org"] == nil)
|
||||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room2:matrix.org"] != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Beacon info updates
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func beaconInfoUpdateFromAnotherDeviceRemovesActiveSession() async throws {
|
||||||
|
setUp()
|
||||||
|
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||||
|
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||||
|
|
||||||
|
try await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)).get()
|
||||||
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil)
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0["!room:matrix.org"] == nil }
|
||||||
|
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: "!room:matrix.org", eventID: "$external_event:matrix.org", isLive: true))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
#expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Reduced accuracy
|
// MARK: - Reduced accuracy
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -185,6 +216,8 @@ final class LiveLocationManagerTests {
|
|||||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||||
try result.get()
|
try result.get()
|
||||||
|
|
||||||
|
try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org")
|
||||||
|
|
||||||
#expect(locationManagerMock.startUpdatingLocationCalled)
|
#expect(locationManagerMock.startUpdatingLocationCalled)
|
||||||
#expect(locationManagerMock.desiredAccuracy == kCLLocationAccuracyReduced)
|
#expect(locationManagerMock.desiredAccuracy == kCLLocationAccuracyReduced)
|
||||||
|
|
||||||
@@ -197,7 +230,7 @@ final class LiveLocationManagerTests {
|
|||||||
|
|
||||||
private func makeRoomProxy(roomID: String) -> JoinedRoomProxyMock {
|
private func makeRoomProxy(roomID: String) -> JoinedRoomProxyMock {
|
||||||
let roomProxy = JoinedRoomProxyMock(.init(id: roomID))
|
let roomProxy = JoinedRoomProxyMock(.init(id: roomID))
|
||||||
roomProxy.startLiveLocationShareDurationReturnValue = .success(())
|
roomProxy.startLiveLocationShareDurationReturnValue = .success("$event:matrix.org")
|
||||||
roomProxy.stopLiveLocationShareReturnValue = .success(())
|
roomProxy.stopLiveLocationShareReturnValue = .success(())
|
||||||
return roomProxy
|
return roomProxy
|
||||||
}
|
}
|
||||||
@@ -205,7 +238,15 @@ final class LiveLocationManagerTests {
|
|||||||
private func setUp(accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy) {
|
private func setUp(accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy) {
|
||||||
appSettings = AppSettings()
|
appSettings = AppSettings()
|
||||||
clientProxy = ClientProxyMock(.init())
|
clientProxy = ClientProxyMock(.init())
|
||||||
|
beaconInfoSubject = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>()
|
||||||
|
clientProxy.liveLocationOwnInfoUpdatesPublisher = beaconInfoSubject.eraseToAnyPublisher()
|
||||||
locationManagerMock = CLLocationManagerMock(.init(accuracyAuthorization: accuracyAuthorization))
|
locationManagerMock = CLLocationManagerMock(.init(accuracyAuthorization: accuracyAuthorization))
|
||||||
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock)
|
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings, locationManager: locationManagerMock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func simulateBeaconEcho(roomID: String, eventID: String) async throws {
|
||||||
|
let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0[roomID] != nil }
|
||||||
|
beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: roomID, eventID: eventID, isLive: true))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user