mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -15,6 +15,7 @@ struct LocationSharingScreenCoordinatorParameters {
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
let timelineController: TimelineControllerProtocol
|
||||
let liveLocationManager: LiveLocationManagerProtocol
|
||||
let appSettings: AppSettings
|
||||
let appMediator: AppMediatorProtocol
|
||||
let analytics: AnalyticsServiceProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
@@ -44,6 +45,7 @@ final class LocationSharingScreenCoordinator: CoordinatorProtocol {
|
||||
roomProxy: parameters.roomProxy,
|
||||
timelineController: parameters.timelineController,
|
||||
liveLocationManager: parameters.liveLocationManager,
|
||||
appSettings: parameters.appSettings,
|
||||
analytics: parameters.analytics,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
mediaProvider: parameters.mediaProvider)
|
||||
|
||||
@@ -80,6 +80,8 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
var userProfiles: [String: UserProfileProxy]
|
||||
var liveLocationShares: [LiveLocationShare] = []
|
||||
var isStoppingLiveLocation = false
|
||||
/// Whether this device has an active live location sharing session in the room.
|
||||
var isSharingLiveLocationOnThisDevice = false
|
||||
|
||||
var annotations: [LocationAnnotation] {
|
||||
switch interactionMode {
|
||||
@@ -111,6 +113,17 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
userID == ownUserID
|
||||
}
|
||||
|
||||
/// Whether the own user has an active live location share in the room, from any of their devices.
|
||||
var isOwnUserSharingLiveLocation: Bool {
|
||||
liveLocationShares.contains { isOwnUser($0.userID) }
|
||||
}
|
||||
|
||||
/// Whether the own user's live location share in the room originates from this very device,
|
||||
/// requiring both the room's share and this device's sharing session to be active.
|
||||
var isOwnUserSharingLiveLocationOnThisDevice: Bool {
|
||||
isOwnUserSharingLiveLocation && isSharingLiveLocationOnThisDevice
|
||||
}
|
||||
|
||||
var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide)
|
||||
|
||||
/// Indicates whether the user is sharing his current location
|
||||
@@ -118,6 +131,14 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow
|
||||
}
|
||||
|
||||
/// Whether the map is centered on the user, following either their dot or their own live marker.
|
||||
var isMapCenteredOnUser: Bool {
|
||||
if case .hideAndFollowMarker = bindings.showsUserLocationMode {
|
||||
return true
|
||||
}
|
||||
return isSharingUserLocation
|
||||
}
|
||||
|
||||
var initialMapCenter: CLLocationCoordinate2D {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
@@ -141,12 +162,17 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
if case .picker = interactionMode {
|
||||
// In picker mode permissions are requested immediately so returns true
|
||||
// if the user's location has not yet been determined while location permissions are given or not yet set
|
||||
!bindings.hasLoadedUserLocation && bindings.isLocationAuthorized != false
|
||||
} else {
|
||||
// In other modes permissions are requested only if the center to user button is tapped
|
||||
// So we only display the loader if the user's location has not yet been determined while location permissions are given.
|
||||
!bindings.hasLoadedUserLocation && bindings.isLocationAuthorized == true
|
||||
return !bindings.hasLoadedUserLocation && bindings.isLocationAuthorized != false
|
||||
}
|
||||
|
||||
// The dot is hidden in favour of the own user's marker, there is no location to wait for.
|
||||
if case .viewLive = interactionMode, isOwnUserSharingLiveLocationOnThisDevice {
|
||||
return false
|
||||
}
|
||||
|
||||
// In other modes permissions are requested only if the center to user button is tapped
|
||||
// So we only display the loader if the user's location has not yet been determined while location permissions are given.
|
||||
return !bindings.hasLoadedUserLocation && bindings.isLocationAuthorized == true
|
||||
}
|
||||
|
||||
var zoomLevel: Double {
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -97,7 +97,7 @@ struct LocationSharingScreen: View {
|
||||
.tint(.compound.iconPrimary)
|
||||
.padding(13)
|
||||
} else {
|
||||
CompoundIcon(context.viewState.isSharingUserLocation ? \.locationNavigatorCentred : \.locationNavigator)
|
||||
CompoundIcon(context.viewState.isMapCenteredOnUser ? \.locationNavigatorCentred : \.locationNavigator)
|
||||
.foregroundStyle(.compound.iconPrimary)
|
||||
.padding(13)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user