Hide user dot and track own user marker in Live Location mode. (#5707)

* added a new tracking mode for following a marker and the logic for switching such tracking mode

* implemented the follow marker tracking mode

* improved and added new tests
This commit is contained in:
Mauro
2026-06-05 13:51:57 +02:00
committed by GitHub
parent 4fed491853
commit da5360c8e8
13 changed files with 372 additions and 64 deletions
@@ -17,6 +17,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
private let roomProxy: JoinedRoomProxyProtocol
private let timelineController: TimelineControllerProtocol
private let liveLocationManager: LiveLocationManagerProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsServiceProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let notificationCenter: NotificationCenter
@@ -36,6 +37,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
roomProxy: JoinedRoomProxyProtocol,
timelineController: TimelineControllerProtocol,
liveLocationManager: LiveLocationManagerProtocol,
appSettings: AppSettings,
analytics: AnalyticsServiceProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
mediaProvider: MediaProviderProtocol,
@@ -43,6 +45,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
self.roomProxy = roomProxy
self.timelineController = timelineController
self.liveLocationManager = liveLocationManager
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.notificationCenter = notificationCenter
@@ -74,11 +77,12 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
let uncertainty = state.isSharingUserLocation ? context.geolocationUncertainty : nil
Task { await sendLocation(.init(coordinate: coordinate, uncertainty: uncertainty), isUserLocation: state.isSharingUserLocation) }
case .userDidPan:
state.bindings.showsUserLocationMode = .show
state.bindings.showsUserLocationMode = state.isOwnUserSharingLiveLocationOnThisDevice ? .hide : .show
case .centerToUser:
switch state.bindings.isLocationAuthorized {
case .some(true), .none:
state.bindings.showsUserLocationMode = .showAndFollow
// While sharing from this device the dot stays hidden, the own user's marker gets followed instead.
state.bindings.showsUserLocationMode = state.isOwnUserSharingLiveLocationOnThisDevice ? .hideAndFollowMarker(id: state.ownUserID) : .showAndFollow
case .some(false):
let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) }
state.bindings.alertInfo = .init(alertID: .missingAuthorization,
@@ -88,7 +92,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
case .stopLiveLocation:
stopLiveLocation()
case .setMapCenter(let coordinate):
state.bindings.showsUserLocationMode = .show
state.bindings.showsUserLocationMode = state.isOwnUserSharingLiveLocationOnThisDevice ? .hide : .show
state.bindings.mapCenterLocation = coordinate
}
}
@@ -100,9 +104,33 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
if let index = state.liveLocationShares.firstIndex(where: { $0.userID == roomProxy.ownUserID }) {
state.liveLocationShares.remove(at: index)
}
updateUserLocationDotVisibility()
Task { await liveLocationManager.stopLiveLocation(roomID: roomProxy.id) }
}
/// Hides the user location dot while the own user is sharing their live location from this
/// device, since their marker already represents it, and restores it once the share has
/// ended or when it comes from another device of theirs.
private func updateUserLocationDotVisibility() {
if state.isOwnUserSharingLiveLocationOnThisDevice {
switch state.bindings.showsUserLocationMode {
case .show:
state.bindings.showsUserLocationMode = .hide
case .showAndFollow:
state.bindings.showsUserLocationMode = .hideAndFollowMarker(id: state.ownUserID)
case .hide, .hideAndFollowMarker:
break
}
} else {
switch state.bindings.showsUserLocationMode {
case .hide, .hideAndFollowMarker:
state.bindings.showsUserLocationMode = .show
case .show, .showAndFollow:
break
}
}
}
private func setupLiveLocationSubscription() async {
let liveLocationService = await roomProxy.makeLiveLocationService()
self.liveLocationService = liveLocationService
@@ -123,6 +151,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
}
updateUserProfiles(members: roomProxy.membersPublisher.value)
updateUserLocationDotVisibility()
if needsCenteringOnFirstLiveLocationUpdate,
let liveLocation = state.liveLocationShares.first,
@@ -140,6 +169,16 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
}
.store(in: &cancellables)
appSettings.$liveLocationSharingSessionsByRoomID
.map { [roomID = roomProxy.id] sessions in sessions[roomID] != nil }
.removeDuplicates()
.sink { [weak self] isSharingLiveLocationOnThisDevice in
guard let self else { return }
state.isSharingLiveLocationOnThisDevice = isSharingLiveLocationOnThisDevice
updateUserLocationDotVisibility()
}
.store(in: &cancellables)
notificationCenter.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
// Let's remove the subscription if the user backgrounds the app (maybe to change their location settings)
@@ -218,14 +257,14 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
}
private func showLiveLocationFlow() {
if liveLocationManager.hasDisplayedLiveLocationDisclaimer {
if appSettings.liveLocationDisclaimerDisplayed {
showLiveLocationDurationPicker()
} else {
state.bindings.alertInfo = .init(alertID: .liveLocationDisclaimer,
primaryButton: .init(title: L10n.actionDecline, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionAccept) { [weak self] in
guard let self else { return }
liveLocationManager.hasDisplayedLiveLocationDisclaimer = true
appSettings.liveLocationDisclaimerDisplayed = true
// Delay so SwiftUI finishes dismissing the current alert
// before presenting the next one.
DispatchQueue.main.async {
@@ -389,11 +428,14 @@ extension LocationSharingScreenViewModel {
let roomProxy = JoinedRoomProxyMock(.init(members: .allMembers, ownUserID: RoomMemberProxyMock.mockMe.userID))
roomProxy.makeLiveLocationServiceReturnValue = liveLocationServiceMock
let appSettings = AppSettings.volatile()
return LocationSharingScreenViewModel(interactionMode: interactionMode,
mapURLBuilder: AppSettings.volatile().mapTilerSettings.publisher.value,
mapURLBuilder: appSettings.mapTilerSettings.publisher.value,
roomProxy: roomProxy,
timelineController: TimelineControllerMock(.init()),
liveLocationManager: LiveLocationManagerMock(),
appSettings: appSettings,
analytics: AnalyticsServiceMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(.init()))