From 693a76ecf035e29858d7140f2ca9a5239b88975c Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 20 Apr 2026 20:26:11 +0200 Subject: [PATCH] allow users to set live location minimum distance update --- .../Application/Settings/AppSettings.swift | 4 +++ .../Mocks/Generated/GeneratedMocks.swift | 5 ++++ .../AdvancedSettingsScreenModels.swift | 23 +++++++++++++++-- .../View/AdvancedSettingsScreen.swift | 25 +++++++++++++++++++ .../Location/CLLocationManagerProtocol.swift | 1 + .../Location/LiveLocationManager.swift | 24 ++++++++++++++++-- 6 files changed, 78 insertions(+), 4 deletions(-) diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index bd31a379f..756333367 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -66,6 +66,7 @@ final class AppSettings { case voiceMessagePlaybackSpeed case liveLocationSharingTimeoutDatesByRoomID + case liveLocationMinimumDistanceUpdate // Feature flags case publicSearchEnabled @@ -349,6 +350,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.liveLocationSharingTimeoutDatesByRoomID, defaultValue: [String: Date](), storageType: .userDefaults(store)) var liveLocationSharingTimeoutDatesByRoomID + @UserPreference(key: UserDefaultsKeys.liveLocationMinimumDistanceUpdate, defaultValue: 10, storageType: .userDefaults(store)) + var liveLocationMinimumDistanceUpdate + @UserPreference(key: UserDefaultsKeys.frequentlyUsedSystemEmojis, defaultValue: [FrequentlyUsedEmoji](), storageType: .userDefaults(store)) var frequentlyUsedSystemEmojis diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index d1dce52bb..06a22c5ed 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2199,6 +2199,11 @@ class CLLocationManagerMock: CLLocationManagerProtocol, @unchecked Sendable { set(value) { underlyingDesiredAccuracy = value } } var underlyingDesiredAccuracy: CLLocationAccuracy! + var distanceFilter: CLLocationDistance { + get { return underlyingDistanceFilter } + set(value) { underlyingDistanceFilter = value } + } + var underlyingDistanceFilter: CLLocationDistance! var pausesLocationUpdatesAutomatically: Bool { get { return underlyingPausesLocationUpdatesAutomatically } set(value) { underlyingPausesLocationUpdatesAutomatically = value } diff --git a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift index 066bd4bc4..5dc19d436 100644 --- a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift @@ -7,12 +7,30 @@ // import Foundation +import UIKit struct AdvancedSettingsScreenViewState: BindableState { + init(timelineMediaVisibility: TimelineMediaVisibility, hideInviteAvatars: Bool, isWaitingTimelineMediaVisibility: Bool = false, isWaitingHideInviteAvatars: Bool = false, bindings: AdvancedSettingsScreenViewStateBindings) { + self.timelineMediaVisibility = timelineMediaVisibility + self.hideInviteAvatars = hideInviteAvatars + self.isWaitingTimelineMediaVisibility = isWaitingTimelineMediaVisibility + self.isWaitingHideInviteAvatars = isWaitingHideInviteAvatars + self.bindings = bindings + + let linkPlaceholder = "{link}" + var footerString = AttributedString(L10n.screenAdvancedSettingsLiveLocationSectionFooter(linkPlaceholder)) + var linkString = AttributedString(L10n.screenAdvancedSettingsLiveLocationSectionFooterLink) + linkString.link = URL(string: UIApplication.openSettingsURLString) + linkString.bold() + footerString.replace(linkPlaceholder, with: linkString) + liveLocationUpdateFooterAttributedString = footerString + } + + let liveLocationUpdateFooterAttributedString: AttributedString var timelineMediaVisibility: TimelineMediaVisibility var hideInviteAvatars: Bool - var isWaitingTimelineMediaVisibility = false - var isWaitingHideInviteAvatars = false + var isWaitingTimelineMediaVisibility: Bool + var isWaitingHideInviteAvatars: Bool var bindings: AdvancedSettingsScreenViewStateBindings } @@ -42,6 +60,7 @@ protocol AdvancedSettingsProtocol: AnyObject { var appAppearance: AppAppearance { get set } var sharePresence: Bool { get set } var optimizeMediaUploads: Bool { get set } + var liveLocationMinimumDistanceUpdate: Int { get set } } extension AppSettings: AdvancedSettingsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/View/AdvancedSettingsScreen.swift b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/View/AdvancedSettingsScreen.swift index 20335f6aa..690022131 100644 --- a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/View/AdvancedSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/View/AdvancedSettingsScreen.swift @@ -37,6 +37,7 @@ struct AdvancedSettingsScreen: View { moderationAndSafetySection timelineMediaSection + liveLocationSection } .compoundList() .navigationTitle(L10n.commonAdvancedSettings) @@ -84,6 +85,30 @@ struct AdvancedSettingsScreen: View { .compoundListSectionFooter() } } + + private var liveLocationSection: some View { + Section { + ListRow(kind: .custom { + Stepper(L10n.screenAdvancedSettingsLiveLocationUpdateDistance(context.liveLocationMinimumDistanceUpdate), + value: $context.liveLocationMinimumDistanceUpdate, in: 1...100) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + .padding(.horizontal, ListRowPadding.horizontal) + .padding(.vertical, ListRowPadding.vertical) + }) + } header: { + VStack(alignment: .leading, spacing: 4) { + Text(L10n.screenAdvancedSettingsLiveLocationSectionTitle) + .compoundListSectionHeader() + Text(L10n.screenAdvancedSettingsLiveLocationSectionDescription) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + } + } footer: { + Text(context.viewState.liveLocationUpdateFooterAttributedString) + .compoundListSectionFooter() + } + } } private extension AppAppearance { diff --git a/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift index 0aa60812f..26e537622 100644 --- a/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift +++ b/ElementX/Sources/Services/Location/CLLocationManagerProtocol.swift @@ -12,6 +12,7 @@ protocol CLLocationManagerProtocol: AnyObject { var delegate: CLLocationManagerDelegate? { get set } var allowsBackgroundLocationUpdates: Bool { get set } var desiredAccuracy: CLLocationAccuracy { get set } + var distanceFilter: CLLocationDistance { get set } var pausesLocationUpdatesAutomatically: Bool { get set } var authorizationStatus: CLAuthorizationStatus { get } diff --git a/ElementX/Sources/Services/Location/LiveLocationManager.swift b/ElementX/Sources/Services/Location/LiveLocationManager.swift index 530099aea..9e683b461 100644 --- a/ElementX/Sources/Services/Location/LiveLocationManager.swift +++ b/ElementX/Sources/Services/Location/LiveLocationManager.swift @@ -47,9 +47,8 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana self.locationManager.delegate = self self.locationManager.allowsBackgroundLocationUpdates = true - self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters self.locationManager.pausesLocationUpdatesAutomatically = false - + setupMinumDistance(appSettings.liveLocationMinimumDistanceUpdate) setupSubscriptions() } @@ -162,6 +161,27 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana locationUpdatesTask = nil } .store(in: &cancellables) + + appSettings.$liveLocationMinimumDistanceUpdate + .removeDuplicates() + .debounce(for: .seconds(3), scheduler: DispatchQueue.main) + .sink { [weak self] minimumDistance in + self?.setupMinumDistance(minimumDistance) + } + .store(in: &cancellables) + } + + /// Sets up the distance filter and the most optimal accuracy given the minimum distance to save battery, + private func setupMinumDistance(_ minimumDistance: Int) { + switch minimumDistance { + case 0..<10: + locationManager.desiredAccuracy = kCLLocationAccuracyBest + case 10..<100: + locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters + default: + locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters + } + locationManager.distanceFilter = CLLocationDistance(minimumDistance) } private func syncActiveRoomProxies(with sessions: [String: Date]) {