From 0fb321576dd92b3b59f52bccaa5bfc5e2539f30f Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 2 Apr 2026 21:53:49 +0200 Subject: [PATCH] implement the start live location handling in the `LocationSharingScreenViewModel` --- .../en-US.lproj/Localizable.strings | 8 +++ .../en.lproj/Localizable.strings | 8 +++ ElementX/Sources/Generated/Strings.swift | 18 +++++ .../LocationSharingScreenModels.swift | 36 ++++++---- .../LocationSharingScreenViewModel.swift | 66 +++++++++++++++++-- 5 files changed, 118 insertions(+), 18 deletions(-) diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index 5205cf4f0..f8c0d98e5 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -680,6 +680,13 @@ "screen_media_upload_preview_item_count" = "Item %1$d of %2$d"; "screen_media_upload_preview_optimize_image_quality_title" = "Optimize image quality"; "screen_media_upload_preview_processing" = "Processing..."; +"screen_missing_key_backup_open_element_classic" = "Open Element Classic"; +"screen_missing_key_backup_step_1" = "Open Element Classic on your device"; +"screen_missing_key_backup_step_2" = "Go to \"Settings\" > \"Security &Privacy\""; +"screen_missing_key_backup_step_3" = "In the section \"Cryptography Keys Management\", click on \"Encrypted Messages Recovery\""; +"screen_missing_key_backup_step_4" = "Follow instructions"; +"screen_missing_key_backup_step_5" = "Done!"; +"screen_missing_key_backup_title" = "We need you to enable your key storage before processing to %1$@"; "screen_onboarding_welcome_back" = "Welcome back"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; @@ -758,6 +765,7 @@ "screen_security_and_privacy_room_visibility_section_footer" = "Addresses are a way to find and access rooms and spaces. This also ensures you can easily share them with others."; "screen_security_and_privacy_room_visibility_section_header" = "Visibility"; "screen_security_and_privacy_title" = "Security & privacy"; +"screen_share_location_live_location_disclaimer_title" = "Your live location history will be stored in the room and visible to members after the session ends."; "screen_share_location_live_location_duration_picker_title" = "Choose how long to share your live location."; "screen_sharing_location_option_sheet_title" = "Sharing options"; "screen_space_add_room_action" = "Room"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 6d1d36699..c1636eaa3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -680,6 +680,13 @@ "screen_media_upload_preview_item_count" = "Item %1$d of %2$d"; "screen_media_upload_preview_optimize_image_quality_title" = "Optimise image quality"; "screen_media_upload_preview_processing" = "Processing..."; +"screen_missing_key_backup_open_element_classic" = "Open Element Classic"; +"screen_missing_key_backup_step_1" = "Open Element Classic on your device"; +"screen_missing_key_backup_step_2" = "Go to \"Settings\" > \"Security &Privacy\""; +"screen_missing_key_backup_step_3" = "In the section \"Cryptography Keys Management\", click on \"Encrypted Messages Recovery\""; +"screen_missing_key_backup_step_4" = "Follow instructions"; +"screen_missing_key_backup_step_5" = "Done!"; +"screen_missing_key_backup_title" = "We need you to enable your key storage before processing to %1$@"; "screen_onboarding_welcome_back" = "Welcome back"; "screen_pinned_timeline_empty_state_description" = "Press on a message and choose “%1$@” to include here."; "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; @@ -758,6 +765,7 @@ "screen_security_and_privacy_room_visibility_section_footer" = "Addresses are a way to find and access rooms and spaces. This also ensures you can easily share them with others."; "screen_security_and_privacy_room_visibility_section_header" = "Visibility"; "screen_security_and_privacy_title" = "Security & privacy"; +"screen_share_location_live_location_disclaimer_title" = "Your live location history will be stored in the room and visible to members after the session ends."; "screen_share_location_live_location_duration_picker_title" = "Choose how long to share your live location."; "screen_sharing_location_option_sheet_title" = "Sharing options"; "screen_space_add_room_action" = "Room"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index fdbbfe37c..4e35300b3 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -2200,6 +2200,22 @@ internal enum L10n { internal static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") } /// Setting up your account. internal static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") } + /// Open Element Classic + internal static var screenMissingKeyBackupOpenElementClassic: String { return L10n.tr("Localizable", "screen_missing_key_backup_open_element_classic") } + /// Open Element Classic on your device + internal static var screenMissingKeyBackupStep1: String { return L10n.tr("Localizable", "screen_missing_key_backup_step_1") } + /// Go to "Settings" > "Security &Privacy" + internal static var screenMissingKeyBackupStep2: String { return L10n.tr("Localizable", "screen_missing_key_backup_step_2") } + /// In the section "Cryptography Keys Management", click on "Encrypted Messages Recovery" + internal static var screenMissingKeyBackupStep3: String { return L10n.tr("Localizable", "screen_missing_key_backup_step_3") } + /// Follow instructions + internal static var screenMissingKeyBackupStep4: String { return L10n.tr("Localizable", "screen_missing_key_backup_step_4") } + /// Done! + internal static var screenMissingKeyBackupStep5: String { return L10n.tr("Localizable", "screen_missing_key_backup_step_5") } + /// We need you to enable your key storage before processing to %1$@ + internal static func screenMissingKeyBackupTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_missing_key_backup_title", String(describing: p1)) + } /// You can change your settings later. internal static var screenNotificationOptinSubtitle: String { return L10n.tr("Localizable", "screen_notification_optin_subtitle") } /// Allow notifications and never miss a message @@ -3183,6 +3199,8 @@ internal enum L10n { internal static var screenSessionVerificationWaitingToAcceptSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_subtitle") } /// Waiting to accept request internal static var screenSessionVerificationWaitingToAcceptTitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_title") } + /// Your live location history will be stored in the room and visible to members after the session ends. + internal static var screenShareLocationLiveLocationDisclaimerTitle: String { return L10n.tr("Localizable", "screen_share_location_live_location_disclaimer_title") } /// Choose how long to share your live location. internal static var screenShareLocationLiveLocationDurationPickerTitle: String { return L10n.tr("Localizable", "screen_share_location_live_location_duration_picker_title") } /// Share location diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift index dd22887ba..2dbd8706f 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift @@ -10,9 +10,11 @@ import CoreLocation import Foundation import MatrixRustSDK -enum LocationSharingViewError: Error, Hashable { +enum LocationSharingViewAlert: Hashable { case missingAuthorization case missingAlwaysAuthorization + case liveLocationDisclaimer + case liveLocationDurationSelection case mapError(MapLibreError) } @@ -130,12 +132,12 @@ struct LocationSharingScreenBindings { return nil } set { - alertInfo = newValue.map { AlertInfo(locationSharingViewError: .mapError($0)) } + alertInfo = newValue.map { AlertInfo(alertID: .mapError($0)) } } } /// Information describing the currently displayed alert. - var alertInfo: AlertInfo? + var alertInfo: AlertInfo? var showShareSheet = false } @@ -148,30 +150,42 @@ enum LocationSharingScreenViewAction { case userDidPan } -extension AlertInfo where T == LocationSharingViewError { - init(locationSharingViewError error: LocationSharingViewError, +extension AlertInfo where T == LocationSharingViewAlert { + init(alertID: LocationSharingViewAlert, primaryButton: AlertButton = AlertButton(title: L10n.actionOk, action: nil), - secondaryButton: AlertButton? = nil) { - switch error { + secondaryButton: AlertButton? = nil, + verticalButtons: [AlertButton]? = nil) { + switch alertID { case .missingAuthorization: - self.init(id: error, + self.init(id: alertID, title: L10n.dialogAllowAccess, message: L10n.dialogPermissionLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName), primaryButton: primaryButton, secondaryButton: secondaryButton) case .missingAlwaysAuthorization: - self.init(id: error, + self.init(id: alertID, title: L10n.dialogAllowAccess, message: L10n.dialogPermissionLiveLocationDescriptionIos(InfoPlistReader.main.bundleDisplayName), primaryButton: primaryButton, secondaryButton: secondaryButton) + 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: error, + self.init(id: alertID, title: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName), primaryButton: primaryButton, secondaryButton: secondaryButton) case .mapError(.failedLocatingUser): - self.init(id: error, + self.init(id: alertID, title: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName), primaryButton: primaryButton, secondaryButton: secondaryButton) diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift index 3e2aa2d05..d3734eddc 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift @@ -72,7 +72,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati state.bindings.showsUserLocationMode = .showAndFollow case .some(false): let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) } - state.bindings.alertInfo = .init(locationSharingViewError: .missingAuthorization, + state.bindings.alertInfo = .init(alertID: .missingAuthorization, primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil), secondaryButton: .init(title: L10n.commonSettings, action: action)) } @@ -112,13 +112,23 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati } } + private static let durationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .full + formatter.allowedUnits = [.hour, .minute] + return formatter + }() + private func startLiveLocationSharing() { + requestAlwaysLocationPermission() + } + + private func requestAlwaysLocationPermission() { authorizationStatusSubscription = nil let authorizationStatus = liveLocationManager.authorizationStatus.value switch authorizationStatus { case .authorizedAlways: - // TODO: Start sending live location updates to the room - break + showLiveLocationDisclaimer() case .notDetermined: // This is to solve a race condition with map libre which always tries first // to request the when in use permission, we wait for it and then try again @@ -127,7 +137,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati .first() // this publisher only fires when there is an actual change, and if the user is done with permissions .sink { [weak self] newValue in guard newValue == .authorizedWhenInUse else { return } - self?.startLiveLocationSharing() + self?.requestAlwaysLocationPermission() } case .authorizedWhenInUse: guard liveLocationManager.requestAlwaysAuthorizationIfPossible() else { @@ -139,17 +149,59 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati authorizationStatusSubscription = liveLocationManager.authorizationStatus .filter { $0 != authorizationStatus } // skip current status .first() // this publisher only fires when there is an actual change, and if the user is done with permissions - .sink { newValue in + .sink { [weak self] newValue in guard newValue == .authorizedAlways else { return } - // TODO: Start sending live location updates to the room + self?.showLiveLocationDisclaimer() } default: showMissingAlwaysAuthorizedAlert() } } + private func showLiveLocationDisclaimer() { + state.bindings.alertInfo = .init(alertID: .liveLocationDisclaimer, + primaryButton: .init(title: L10n.actionDecline, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.actionAccept) { [weak self] in + // Delay so SwiftUI finishes dismissing the current alert + // before presenting the next one. + DispatchQueue.main.async { + self?.showLiveLocationDurationPicker() + } + }) + } + + private func showLiveLocationDurationPicker() { + let durations: [TimeInterval] = [15 * 60, // 15 minutes + 60 * 60, // 1 hour + 8 * 60 * 60] // 8 hours + + let durationButtons: [AlertInfo.AlertButton] = durations.compactMap { duration in + guard let title = Self.durationFormatter.string(from: duration) else { return nil } + return .init(title: title) { [weak self] in + Task { [weak self] in await self?.startLiveLocationSharingInRoom(durationMillis: UInt64(duration * 1000)) } + } + } + + state.bindings.alertInfo = .init(alertID: .liveLocationDurationSelection, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + verticalButtons: durationButtons) + } + + private func startLiveLocationSharingInRoom(durationMillis: UInt64) async { + let result = await liveLocationManager.startLiveLocation(roomID: roomProxy.id, + durationMillis: durationMillis) + + switch result { + case .success: + actionsSubject.send(.close) + case .failure(let error): + MXLog.error("Failed to start live location sharing: \(error)") + showErrorIndicator() + } + } + private func showMissingAlwaysAuthorizedAlert() { - state.bindings.alertInfo = .init(locationSharingViewError: .missingAlwaysAuthorization, + state.bindings.alertInfo = .init(alertID: .missingAlwaysAuthorization, primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil), secondaryButton: .init(title: L10n.commonSettings) { [weak self] in self?.actionsSubject.send(.openSystemSettings) }) }