mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
implement the start, send and stop functions
This commit is contained in:
@@ -80,6 +80,7 @@ final class AppSettings {
|
|||||||
case focusEventOnNotificationTap
|
case focusEventOnNotificationTap
|
||||||
case linkNewDeviceEnabled
|
case linkNewDeviceEnabled
|
||||||
case liveLocationSharingEnabled
|
case liveLocationSharingEnabled
|
||||||
|
case liveLocationSharingTimeoutDatesByRoomID
|
||||||
case floatingTimelineDateEnabled
|
case floatingTimelineDateEnabled
|
||||||
|
|
||||||
// Doug's tweaks 🔧
|
// Doug's tweaks 🔧
|
||||||
@@ -349,6 +350,9 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.hasRequestedLocationAlwaysLocationAuthorization, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.hasRequestedLocationAlwaysLocationAuthorization, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var hasRequestedLocationAlwaysLocationAuthorization
|
var hasRequestedLocationAlwaysLocationAuthorization
|
||||||
|
|
||||||
|
@UserPreference(key: UserDefaultsKeys.liveLocationSharingTimeoutDatesByRoomID, defaultValue: [String: Date](), storageType: .userDefaults(store))
|
||||||
|
var liveLocationSharingTimeoutDatesByRoomID
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.frequentlyUsedSystemEmojis, defaultValue: [FrequentlyUsedEmoji](), storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.frequentlyUsedSystemEmojis, defaultValue: [FrequentlyUsedEmoji](), storageType: .userDefaults(store))
|
||||||
var frequentlyUsedSystemEmojis
|
var frequentlyUsedSystemEmojis
|
||||||
|
|
||||||
|
|||||||
@@ -10062,6 +10062,210 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
|
|||||||
return clearDraftThreadRootEventIDReturnValue
|
return clearDraftThreadRootEventIDReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - startLiveLocationShare
|
||||||
|
|
||||||
|
var startLiveLocationShareDurationMillisUnderlyingCallsCount = 0
|
||||||
|
var startLiveLocationShareDurationMillisCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return startLiveLocationShareDurationMillisUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = startLiveLocationShareDurationMillisUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
startLiveLocationShareDurationMillisUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
startLiveLocationShareDurationMillisUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var startLiveLocationShareDurationMillisCalled: Bool {
|
||||||
|
return startLiveLocationShareDurationMillisCallsCount > 0
|
||||||
|
}
|
||||||
|
var startLiveLocationShareDurationMillisReceivedDurationMillis: UInt64?
|
||||||
|
var startLiveLocationShareDurationMillisReceivedInvocations: [UInt64] = []
|
||||||
|
|
||||||
|
var startLiveLocationShareDurationMillisUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var startLiveLocationShareDurationMillisReturnValue: Result<Void, RoomProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return startLiveLocationShareDurationMillisUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = startLiveLocationShareDurationMillisUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
startLiveLocationShareDurationMillisUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
startLiveLocationShareDurationMillisUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var startLiveLocationShareDurationMillisClosure: ((UInt64) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func startLiveLocationShare(durationMillis: UInt64) async -> Result<Void, RoomProxyError> {
|
||||||
|
startLiveLocationShareDurationMillisCallsCount += 1
|
||||||
|
startLiveLocationShareDurationMillisReceivedDurationMillis = durationMillis
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.startLiveLocationShareDurationMillisReceivedInvocations.append(durationMillis)
|
||||||
|
}
|
||||||
|
if let startLiveLocationShareDurationMillisClosure = startLiveLocationShareDurationMillisClosure {
|
||||||
|
return await startLiveLocationShareDurationMillisClosure(durationMillis)
|
||||||
|
} else {
|
||||||
|
return startLiveLocationShareDurationMillisReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendLiveLocation
|
||||||
|
|
||||||
|
var sendLiveLocationGeoURIUnderlyingCallsCount = 0
|
||||||
|
var sendLiveLocationGeoURICallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return sendLiveLocationGeoURIUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = sendLiveLocationGeoURIUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
sendLiveLocationGeoURIUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
sendLiveLocationGeoURIUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sendLiveLocationGeoURICalled: Bool {
|
||||||
|
return sendLiveLocationGeoURICallsCount > 0
|
||||||
|
}
|
||||||
|
var sendLiveLocationGeoURIReceivedGeoURI: GeoURI?
|
||||||
|
var sendLiveLocationGeoURIReceivedInvocations: [GeoURI] = []
|
||||||
|
|
||||||
|
var sendLiveLocationGeoURIUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendLiveLocationGeoURIReturnValue: Result<Void, RoomProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return sendLiveLocationGeoURIUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = sendLiveLocationGeoURIUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
sendLiveLocationGeoURIUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
sendLiveLocationGeoURIUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sendLiveLocationGeoURIClosure: ((GeoURI) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendLiveLocationGeoURICallsCount += 1
|
||||||
|
sendLiveLocationGeoURIReceivedGeoURI = geoURI
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.sendLiveLocationGeoURIReceivedInvocations.append(geoURI)
|
||||||
|
}
|
||||||
|
if let sendLiveLocationGeoURIClosure = sendLiveLocationGeoURIClosure {
|
||||||
|
return await sendLiveLocationGeoURIClosure(geoURI)
|
||||||
|
} else {
|
||||||
|
return sendLiveLocationGeoURIReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - stopLiveLocationShare
|
||||||
|
|
||||||
|
var stopLiveLocationShareUnderlyingCallsCount = 0
|
||||||
|
var stopLiveLocationShareCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return stopLiveLocationShareUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = stopLiveLocationShareUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
stopLiveLocationShareUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
stopLiveLocationShareUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stopLiveLocationShareCalled: Bool {
|
||||||
|
return stopLiveLocationShareCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopLiveLocationShareUnderlyingReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var stopLiveLocationShareReturnValue: Result<Void, RoomProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return stopLiveLocationShareUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, RoomProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = stopLiveLocationShareUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
stopLiveLocationShareUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
stopLiveLocationShareUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stopLiveLocationShareClosure: (() async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func stopLiveLocationShare() async -> Result<Void, RoomProxyError> {
|
||||||
|
stopLiveLocationShareCallsCount += 1
|
||||||
|
if let stopLiveLocationShareClosure = stopLiveLocationShareClosure {
|
||||||
|
return await stopLiveLocationShareClosure()
|
||||||
|
} else {
|
||||||
|
return stopLiveLocationShareReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class KeychainControllerMock: KeychainControllerProtocol, @unchecked Sendable {
|
class KeychainControllerMock: KeychainControllerProtocol, @unchecked Sendable {
|
||||||
|
|
||||||
@@ -11236,6 +11440,117 @@ class LiveLocationManagerMock: LiveLocationManagerProtocol, @unchecked Sendable
|
|||||||
return requestAlwaysAuthorizationIfPossibleReturnValue
|
return requestAlwaysAuthorizationIfPossibleReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - startLiveLocation
|
||||||
|
|
||||||
|
var startLiveLocationRoomIDDurationMillisUnderlyingCallsCount = 0
|
||||||
|
var startLiveLocationRoomIDDurationMillisCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return startLiveLocationRoomIDDurationMillisUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = startLiveLocationRoomIDDurationMillisUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
startLiveLocationRoomIDDurationMillisUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
startLiveLocationRoomIDDurationMillisUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var startLiveLocationRoomIDDurationMillisCalled: Bool {
|
||||||
|
return startLiveLocationRoomIDDurationMillisCallsCount > 0
|
||||||
|
}
|
||||||
|
var startLiveLocationRoomIDDurationMillisReceivedArguments: (roomID: String, durationMillis: UInt64)?
|
||||||
|
var startLiveLocationRoomIDDurationMillisReceivedInvocations: [(roomID: String, durationMillis: UInt64)] = []
|
||||||
|
|
||||||
|
var startLiveLocationRoomIDDurationMillisUnderlyingReturnValue: Result<Void, LiveLocationManagerError>!
|
||||||
|
var startLiveLocationRoomIDDurationMillisReturnValue: Result<Void, LiveLocationManagerError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return startLiveLocationRoomIDDurationMillisUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, LiveLocationManagerError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = startLiveLocationRoomIDDurationMillisUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
startLiveLocationRoomIDDurationMillisUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
startLiveLocationRoomIDDurationMillisUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var startLiveLocationRoomIDDurationMillisClosure: ((String, UInt64) async -> Result<Void, LiveLocationManagerError>)?
|
||||||
|
|
||||||
|
func startLiveLocation(roomID: String, durationMillis: UInt64) async -> Result<Void, LiveLocationManagerError> {
|
||||||
|
startLiveLocationRoomIDDurationMillisCallsCount += 1
|
||||||
|
startLiveLocationRoomIDDurationMillisReceivedArguments = (roomID: roomID, durationMillis: durationMillis)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.startLiveLocationRoomIDDurationMillisReceivedInvocations.append((roomID: roomID, durationMillis: durationMillis))
|
||||||
|
}
|
||||||
|
if let startLiveLocationRoomIDDurationMillisClosure = startLiveLocationRoomIDDurationMillisClosure {
|
||||||
|
return await startLiveLocationRoomIDDurationMillisClosure(roomID, durationMillis)
|
||||||
|
} else {
|
||||||
|
return startLiveLocationRoomIDDurationMillisReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - stopLiveLocation
|
||||||
|
|
||||||
|
var stopLiveLocationRoomIDUnderlyingCallsCount = 0
|
||||||
|
var stopLiveLocationRoomIDCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return stopLiveLocationRoomIDUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = stopLiveLocationRoomIDUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
stopLiveLocationRoomIDUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
stopLiveLocationRoomIDUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stopLiveLocationRoomIDCalled: Bool {
|
||||||
|
return stopLiveLocationRoomIDCallsCount > 0
|
||||||
|
}
|
||||||
|
var stopLiveLocationRoomIDReceivedRoomID: String?
|
||||||
|
var stopLiveLocationRoomIDReceivedInvocations: [String] = []
|
||||||
|
var stopLiveLocationRoomIDClosure: ((String) async -> Void)?
|
||||||
|
|
||||||
|
func stopLiveLocation(roomID: String) async {
|
||||||
|
stopLiveLocationRoomIDCallsCount += 1
|
||||||
|
stopLiveLocationRoomIDReceivedRoomID = roomID
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.stopLiveLocationRoomIDReceivedInvocations.append(roomID)
|
||||||
|
}
|
||||||
|
await stopLiveLocationRoomIDClosure?(roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class MediaLoaderMock: MediaLoaderProtocol, @unchecked Sendable {
|
class MediaLoaderMock: MediaLoaderProtocol, @unchecked Sendable {
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
|
|
||||||
private let authorizationStatusSubject: CurrentValueSubject<CLAuthorizationStatus, Never>
|
private let authorizationStatusSubject: CurrentValueSubject<CLAuthorizationStatus, Never>
|
||||||
|
|
||||||
|
/// Cached joined room proxies keyed by room ID, kept in sync with the active sessions dictionary.
|
||||||
|
private var activeRoomProxies = [String: JoinedRoomProxyProtocol]()
|
||||||
|
|
||||||
|
/// The running task that iterates over live location updates.
|
||||||
|
@CancellableTask
|
||||||
|
private var locationUpdatesTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> {
|
var authorizationStatus: CurrentValuePublisher<CLAuthorizationStatus, Never> {
|
||||||
authorizationStatusSubject.asCurrentValuePublisher()
|
authorizationStatusSubject.asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
@@ -33,6 +42,11 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
locationManager.delegate = self
|
locationManager.delegate = self
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = true
|
||||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||||
|
locationManager.pausesLocationUpdatesAutomatically = false
|
||||||
|
|
||||||
|
setupSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - LiveLocationManagerProtocol
|
// MARK: - LiveLocationManagerProtocol
|
||||||
@@ -45,6 +59,39 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startLiveLocation(roomID: String, durationMillis: UInt64) async -> Result<Void, LiveLocationManagerError> {
|
||||||
|
guard case .joined(let roomProxy) = await clientProxy.roomForIdentifier(roomID) else {
|
||||||
|
MXLog.error("Failed to resolve joined room for identifier: \(roomID)")
|
||||||
|
return .failure(.roomNotJoined)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await roomProxy.startLiveLocationShare(durationMillis: durationMillis)
|
||||||
|
|
||||||
|
guard case .success = result else {
|
||||||
|
MXLog.error("Failed to start live location share in room: \(roomID)")
|
||||||
|
return .failure(.startFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutDate = Date().addingTimeInterval(TimeInterval(durationMillis) / 1000.0)
|
||||||
|
appSettings.liveLocationSharingTimeoutDatesByRoomID[roomID] = timeoutDate
|
||||||
|
|
||||||
|
return .success(())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopLiveLocation(roomID: String) async {
|
||||||
|
// Best effort: send the stop event to the room regardless of tracking state.
|
||||||
|
if let roomProxy = await resolveRoomProxy(for: roomID) {
|
||||||
|
let result = await roomProxy.stopLiveLocationShare()
|
||||||
|
if case .failure(let error) = result {
|
||||||
|
MXLog.error("Failed to stop live location share in room \(roomID): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always clean up locally.
|
||||||
|
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeValue(forKey: roomID)
|
||||||
|
activeRoomProxies.removeValue(forKey: roomID)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - CLLocationManagerDelegate
|
// MARK: - CLLocationManagerDelegate
|
||||||
|
|
||||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
@@ -53,6 +100,114 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
|||||||
if manager.authorizationStatus == .notDetermined {
|
if manager.authorizationStatus == .notDetermined {
|
||||||
appSettings.hasRequestedLocationAlwaysLocationAuthorization = false
|
appSettings.hasRequestedLocationAlwaysLocationAuthorization = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If authorization was revoked, stop all active sessions.
|
||||||
|
if manager.authorizationStatus != .authorizedAlways {
|
||||||
|
stopAllSessions()
|
||||||
|
}
|
||||||
|
|
||||||
authorizationStatusSubject.send(manager.authorizationStatus)
|
authorizationStatusSubject.send(manager.authorizationStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func setupSubscriptions() {
|
||||||
|
appSettings.$liveLocationSharingTimeoutDatesByRoomID
|
||||||
|
.removeDuplicates()
|
||||||
|
.sink { [weak self] sessions in
|
||||||
|
guard let self else { return }
|
||||||
|
syncActiveRoomProxies(with: sessions)
|
||||||
|
|
||||||
|
if sessions.isEmpty {
|
||||||
|
locationUpdatesTask = nil
|
||||||
|
} else {
|
||||||
|
startLocationUpdatesIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
appSettings.$liveLocationSharingEnabled
|
||||||
|
.filter { !$0 }
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeAll()
|
||||||
|
activeRoomProxies.removeAll()
|
||||||
|
locationUpdatesTask = nil
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncActiveRoomProxies(with sessions: [String: Date]) {
|
||||||
|
// Remove proxies for rooms no longer in the dictionary.
|
||||||
|
let activeRoomIDs = Set(sessions.keys)
|
||||||
|
for roomID in activeRoomProxies.keys where !activeRoomIDs.contains(roomID) {
|
||||||
|
activeRoomProxies.removeValue(forKey: roomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLocationUpdatesIfNeeded() {
|
||||||
|
guard locationUpdatesTask == nil else { return }
|
||||||
|
|
||||||
|
locationUpdatesTask = Task { [weak self] in
|
||||||
|
do {
|
||||||
|
for try await update in CLLocationUpdate.liveUpdates() {
|
||||||
|
guard let self, !Task.isCancelled else { break }
|
||||||
|
|
||||||
|
await self.sendLocationToActiveRooms(update)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Live location updates failed with error: \(error)")
|
||||||
|
self?.stopAllSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendLocationToActiveRooms(_ update: CLLocationUpdate) async {
|
||||||
|
let sessions = appSettings.liveLocationSharingTimeoutDatesByRoomID
|
||||||
|
let geoURI = update.location.map { GeoURI(coordinate: $0.coordinate, uncertainty: $0.horizontalAccuracy) }
|
||||||
|
|
||||||
|
for (roomID, timeoutDate) in sessions {
|
||||||
|
if Date() >= timeoutDate {
|
||||||
|
MXLog.info("Live location session expired for room: \(roomID)")
|
||||||
|
await stopLiveLocation(roomID: roomID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let geoURI else { continue }
|
||||||
|
|
||||||
|
let roomProxy = await resolveRoomProxy(for: roomID)
|
||||||
|
guard let roomProxy else {
|
||||||
|
MXLog.error("Failed to resolve room proxy for live location update in room: \(roomID)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await roomProxy.sendLiveLocation(geoURI: geoURI)
|
||||||
|
if case .failure(let error) = result {
|
||||||
|
MXLog.error("Failed to send live location update to room \(roomID): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolveRoomProxy(for roomID: String) async -> JoinedRoomProxyProtocol? {
|
||||||
|
if let cached = activeRoomProxies[roomID] {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
guard case .joined(let roomProxy) = await clientProxy.roomForIdentifier(roomID) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRoomProxies[roomID] = roomProxy
|
||||||
|
return roomProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopAllSessions() {
|
||||||
|
let roomIDs = Array(appSettings.liveLocationSharingTimeoutDatesByRoomID.keys)
|
||||||
|
Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
for roomID in roomIDs {
|
||||||
|
await stopLiveLocation(roomID: roomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import Combine
|
|||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum LiveLocationManagerError: Error {
|
||||||
|
case roomNotJoined
|
||||||
|
case startFailed
|
||||||
|
}
|
||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol LiveLocationManagerProtocol: AnyObject {
|
protocol LiveLocationManagerProtocol: AnyObject {
|
||||||
/// Publishes the current location authorization status.
|
/// Publishes the current location authorization status.
|
||||||
@@ -20,4 +25,18 @@ protocol LiveLocationManagerProtocol: AnyObject {
|
|||||||
/// `false` if the request was already made before and iOS would silently ignore it.
|
/// `false` if the request was already made before and iOS would silently ignore it.
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func requestAlwaysAuthorizationIfPossible() -> Bool
|
func requestAlwaysAuthorizationIfPossible() -> Bool
|
||||||
|
|
||||||
|
/// Starts sharing live location in a room.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - roomID: The identifier of the room to share live location in.
|
||||||
|
/// - durationMillis: The duration in milliseconds for how long the live location should be shared.
|
||||||
|
func startLiveLocation(roomID: String, durationMillis: UInt64) async -> Result<Void, LiveLocationManagerError>
|
||||||
|
|
||||||
|
/// Stops sharing live location in a room.
|
||||||
|
///
|
||||||
|
/// Sends a stop event to the room (best effort) and removes it from the tracked sessions.
|
||||||
|
/// Can also be used to stop a live location share started by another device.
|
||||||
|
/// - Parameter roomID: The identifier of the room to stop sharing live location in.
|
||||||
|
func stopLiveLocation(roomID: String) async
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -751,6 +751,38 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
return .failure(.sdkError(error))
|
return .failure(.sdkError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Live Location
|
||||||
|
|
||||||
|
func startLiveLocationShare(durationMillis: UInt64) async -> Result<Void, RoomProxyError> {
|
||||||
|
do {
|
||||||
|
try await room.startLiveLocationShare(durationMillis: durationMillis)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed starting live location share with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError> {
|
||||||
|
do {
|
||||||
|
try await room.sendLiveLocation(geoUri: geoURI.string)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed sending live location with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopLiveLocationShare() async -> Result<Void, RoomProxyError> {
|
||||||
|
do {
|
||||||
|
try await room.stopLiveLocationShare()
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed stopping live location share with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
|||||||
func saveDraft(_ draft: ComposerDraft, threadRootEventID: String?) async -> Result<Void, RoomProxyError>
|
func saveDraft(_ draft: ComposerDraft, threadRootEventID: String?) async -> Result<Void, RoomProxyError>
|
||||||
func loadDraft(threadRootEventID: String?) async -> Result<ComposerDraft?, RoomProxyError>
|
func loadDraft(threadRootEventID: String?) async -> Result<ComposerDraft?, RoomProxyError>
|
||||||
func clearDraft(threadRootEventID: String?) async -> Result<Void, RoomProxyError>
|
func clearDraft(threadRootEventID: String?) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
// MARK: - Live Location
|
||||||
|
|
||||||
|
func startLiveLocationShare(durationMillis: UInt64) async -> Result<Void, RoomProxyError>
|
||||||
|
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError>
|
||||||
|
func stopLiveLocationShare() async -> Result<Void, RoomProxyError>
|
||||||
}
|
}
|
||||||
|
|
||||||
extension JoinedRoomProxyProtocol {
|
extension JoinedRoomProxyProtocol {
|
||||||
|
|||||||
Reference in New Issue
Block a user