279 lines
10 KiB
Swift
279 lines
10 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2023-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import CoreLocation
|
|
import Foundation
|
|
import MatrixRustSDK
|
|
|
|
enum LocationSharingViewAlert: Hashable {
|
|
case missingAuthorization
|
|
case missingAlwaysAuthorization
|
|
case missingLiveLocationSharingPermission
|
|
case liveLocationDisclaimer
|
|
case liveLocationDurationSelection
|
|
case mapError(MapLibreError)
|
|
}
|
|
|
|
enum LocationSharingScreenViewModelAction {
|
|
case close
|
|
case openSystemSettings
|
|
}
|
|
|
|
enum LocationSharingInteractionMode: Hashable {
|
|
case picker
|
|
case viewStatic(StaticLocationData)
|
|
case viewLive(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?)
|
|
}
|
|
|
|
struct LocationSharingScreenViewState: BindableState {
|
|
init(interactionMode: LocationSharingInteractionMode,
|
|
mapURLBuilder: MapTilerURLBuilderProtocol,
|
|
ownUserID: String) {
|
|
self.interactionMode = interactionMode
|
|
self.mapURLBuilder = mapURLBuilder
|
|
self.ownUserID = ownUserID
|
|
|
|
let initialProfile: UserProfileProxy = switch interactionMode {
|
|
case .viewStatic(let locationData):
|
|
.init(sender: locationData.sender)
|
|
case .viewLive(let sender, _):
|
|
if let sender {
|
|
.init(sender: sender)
|
|
} else {
|
|
.init(userID: ownUserID)
|
|
}
|
|
case .picker:
|
|
.init(userID: ownUserID)
|
|
}
|
|
userProfiles = [initialProfile.userID: initialProfile]
|
|
|
|
if case .viewLive(_, let initialLiveLocationShare) = interactionMode, let initialLiveLocationShare {
|
|
liveLocationShares = [initialLiveLocationShare]
|
|
}
|
|
|
|
bindings.showsUserLocationMode = switch interactionMode {
|
|
case .picker: .showAndFollow
|
|
case .viewStatic, .viewLive: .show
|
|
}
|
|
}
|
|
|
|
let interactionMode: LocationSharingInteractionMode
|
|
let mapURLBuilder: MapTilerURLBuilderProtocol
|
|
let ownUserID: String
|
|
var userProfiles: [String: UserProfileProxy]
|
|
var liveLocationShares: [LiveLocationShare] = []
|
|
var isStoppingLiveLocation = false
|
|
|
|
var annotations: [LocationAnnotation] {
|
|
switch interactionMode {
|
|
case .viewStatic(let location):
|
|
let profile = userProfiles.values.first
|
|
let kind: LocationMarkerKind = if location.kind == .sender, let profile {
|
|
.staticUser(profile)
|
|
} else {
|
|
.pin
|
|
}
|
|
let coordinate = CLLocationCoordinate2D(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude)
|
|
return [LocationAnnotation(id: kind.id, coordinate: coordinate, kind: kind)]
|
|
case .viewLive:
|
|
return liveLocationShares.compactMap { share in
|
|
guard let geoURI = share.geoURI else { return nil }
|
|
if share.userID == ownUserID, isStoppingLiveLocation { return nil }
|
|
|
|
let profile = userProfiles[share.userID] ?? UserProfileProxy(userID: share.userID)
|
|
let kind = LocationMarkerKind.liveUser(profile)
|
|
let coordinate = CLLocationCoordinate2D(latitude: geoURI.latitude, longitude: geoURI.longitude)
|
|
return LocationAnnotation(id: profile.userID, coordinate: coordinate, kind: kind)
|
|
}
|
|
case .picker:
|
|
return []
|
|
}
|
|
}
|
|
|
|
func isOwnUser(_ userID: String) -> Bool {
|
|
userID == ownUserID
|
|
}
|
|
|
|
var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide)
|
|
|
|
/// Indicates whether the user is sharing his current location
|
|
var isSharingUserLocation: Bool {
|
|
bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow
|
|
}
|
|
|
|
var initialMapCenter: CLLocationCoordinate2D {
|
|
switch interactionMode {
|
|
case .picker:
|
|
// middle point in Europe, to be used if the users location is not yet known
|
|
.init(latitude: 49.843, longitude: 9.902056)
|
|
case .viewStatic(let location):
|
|
.init(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude)
|
|
case .viewLive(_, let initialLiveLocationShare):
|
|
if let initialLiveLocationShare {
|
|
.init(latitude: initialLiveLocationShare.geoURI?.latitude ?? 0,
|
|
longitude: initialLiveLocationShare.geoURI?.longitude ?? 0)
|
|
} else {
|
|
.init(latitude: 49.843, longitude: 9.902056)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Displays a loader if the user's location has not yet been determined
|
|
/// Does not work as intended on simulator.
|
|
var isLocationLoading: Bool {
|
|
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
|
|
}
|
|
}
|
|
|
|
var zoomLevel: Double {
|
|
15.0
|
|
}
|
|
|
|
var initialZoomLevel: Double {
|
|
switch interactionMode {
|
|
case .picker:
|
|
return 2.7
|
|
case .viewStatic, .viewLive:
|
|
return 15.0
|
|
}
|
|
}
|
|
|
|
/// The marker kind used for the picker overlay (not a map annotation).
|
|
var pickerMarkerKind: LocationMarkerKind? {
|
|
guard case .picker = interactionMode else { return nil }
|
|
if let profile = userProfiles.values.first {
|
|
return isSharingUserLocation ? .staticUser(profile) : .pin
|
|
}
|
|
return .pin
|
|
}
|
|
}
|
|
|
|
struct LocationSharingScreenBindings {
|
|
var mapCenterLocation: CLLocationCoordinate2D?
|
|
var geolocationUncertainty: CLLocationAccuracy?
|
|
var showsUserLocationMode: ShowUserLocationMode
|
|
var hasLoadedUserLocation = false
|
|
var isLocationAuthorized: Bool?
|
|
|
|
/// Information describing the currently displayed alert.
|
|
var mapError: MapLibreError? {
|
|
get {
|
|
if case let .mapError(error) = alertInfo?.id {
|
|
return error
|
|
}
|
|
return nil
|
|
}
|
|
set {
|
|
alertInfo = newValue.map { AlertInfo(alertID: .mapError($0)) }
|
|
}
|
|
}
|
|
|
|
/// Information describing the currently displayed alert.
|
|
var alertInfo: AlertInfo<LocationSharingViewAlert>?
|
|
|
|
var sharedAnnotation: LocationAnnotation?
|
|
}
|
|
|
|
enum LocationSharingScreenViewAction {
|
|
case close
|
|
case selectLocation
|
|
case startLiveLocation
|
|
case centerToUser
|
|
case userDidPan
|
|
case stopLiveLocation
|
|
case setMapCenter(CLLocationCoordinate2D)
|
|
}
|
|
|
|
extension AlertInfo where T == LocationSharingViewAlert {
|
|
init(alertID: LocationSharingViewAlert,
|
|
primaryButton: AlertButton = AlertButton(title: L10n.actionOk, action: nil),
|
|
secondaryButton: AlertButton? = nil,
|
|
verticalButtons: [AlertButton]? = nil) {
|
|
switch alertID {
|
|
case .missingAuthorization:
|
|
self.init(id: alertID,
|
|
title: L10n.dialogAllowAccess,
|
|
message: L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName),
|
|
primaryButton: primaryButton,
|
|
secondaryButton: secondaryButton)
|
|
case .missingAlwaysAuthorization:
|
|
self.init(id: alertID,
|
|
title: L10n.dialogAllowAccess,
|
|
message: L10n.dialogPermissionLiveLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName),
|
|
primaryButton: primaryButton,
|
|
secondaryButton: secondaryButton)
|
|
case .missingLiveLocationSharingPermission:
|
|
self.init(id: alertID,
|
|
title: L10n.screenShareLocationLiveLocationMissingPermissions,
|
|
primaryButton: primaryButton)
|
|
case .liveLocationDisclaimer:
|
|
self.init(id: alertID,
|
|
title: L10n.screenShareLocationLiveLocationDisclaimerTitle,
|
|
primaryButton: primaryButton,
|
|
secondaryButton: secondaryButton)
|
|
case .liveLocationDurationSelection:
|
|
self.init(id: alertID,
|
|
title: L10n.screenShareLocationLiveLocationDurationPickerTitle,
|
|
primaryButton: primaryButton,
|
|
secondaryButton: nil,
|
|
verticalButtons: verticalButtons)
|
|
case .mapError(.failedLoadingMap):
|
|
self.init(id: alertID,
|
|
title: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName),
|
|
primaryButton: primaryButton,
|
|
secondaryButton: secondaryButton)
|
|
case .mapError(.failedLocatingUser):
|
|
self.init(id: alertID,
|
|
title: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName),
|
|
primaryButton: primaryButton,
|
|
secondaryButton: secondaryButton)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum LocationMarkerKind: Equatable {
|
|
case pin
|
|
case staticUser(UserProfileProxy)
|
|
case liveUser(UserProfileProxy)
|
|
case placeholder
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .pin, .placeholder:
|
|
UUID().uuidString
|
|
case .staticUser(let profile), .liveUser(let profile):
|
|
profile.userID
|
|
}
|
|
}
|
|
|
|
var displayName: String? {
|
|
switch self {
|
|
case .pin, .placeholder:
|
|
nil
|
|
case .staticUser(let profile), .liveUser(let profile):
|
|
profile.displayName
|
|
}
|
|
}
|
|
|
|
var userProfile: UserProfileProxy? {
|
|
switch self {
|
|
case .pin, .placeholder:
|
|
nil
|
|
case .staticUser(let profile), .liveUser(let profile):
|
|
profile
|
|
}
|
|
}
|
|
}
|