Add a LocationSharingSettingsScreen. (#6281)

This commit is contained in:
Doug
2026-10-07 09:10:15 +01:00
committed by GitHub
parent ce3829f253
commit cf25287308
13 changed files with 257 additions and 0 deletions
@@ -90,6 +90,7 @@ enum TestablePreviewsDictionary {
"LocationPickerSheet_Previews" : LocationPickerSheet_Previews.self,
"LocationRoomTimelineView_Previews" : LocationRoomTimelineView_Previews.self,
"LocationSharingScreen_Previews" : LocationSharingScreen_Previews.self,
"LocationSharingSettingsScreen_Previews" : LocationSharingSettingsScreen_Previews.self,
"LongPressWithFeedback_Previews" : LongPressWithFeedback_Previews.self,
"ManageAuthorizedSpacesScreen_Previews" : ManageAuthorizedSpacesScreen_Previews.self,
"ManageRoomMemberSheetView_Previews" : ManageRoomMemberSheetView_Previews.self,
@@ -0,0 +1,25 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct LocationSharingSettingsScreenCoordinatorParameters {
let userSettings: UserSettings
}
final class LocationSharingSettingsScreenCoordinator: CoordinatorProtocol {
private var viewModel: LocationSharingSettingsScreenViewModelProtocol
init(parameters: LocationSharingSettingsScreenCoordinatorParameters) {
viewModel = LocationSharingSettingsScreenViewModel(userSettings: parameters.userSettings)
}
func toPresentable() -> AnyView {
AnyView(LocationSharingSettingsScreen(context: viewModel.context))
}
}
@@ -0,0 +1,41 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import UIKit
struct LocationSharingSettingsScreenViewState: BindableState {
init(bindings: LocationSharingSettingsScreenViewStateBindings) {
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 bindings: LocationSharingSettingsScreenViewStateBindings
}
struct LocationSharingSettingsScreenViewStateBindings {
private let userSettings: UserSettings
init(userSettings: UserSettings) {
self.userSettings = userSettings
}
var liveLocationMinimumDistanceUpdate: Int {
get { userSettings.liveLocationMinimumDistanceUpdate }
set { userSettings.liveLocationMinimumDistanceUpdate = newValue }
}
}
enum LocationSharingSettingsScreenViewAction { }
@@ -0,0 +1,20 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias LocationSharingSettingsScreenViewModelType = StateStoreViewModelV2<LocationSharingSettingsScreenViewState, LocationSharingSettingsScreenViewAction>
class LocationSharingSettingsScreenViewModel: LocationSharingSettingsScreenViewModelType, LocationSharingSettingsScreenViewModelProtocol {
init(userSettings: UserSettings) {
let state = LocationSharingSettingsScreenViewState(bindings: .init(userSettings: userSettings))
super.init(initialViewState: state)
}
override func process(viewAction: LocationSharingSettingsScreenViewAction) { }
}
@@ -0,0 +1,12 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
protocol LocationSharingSettingsScreenViewModelProtocol {
var context: LocationSharingSettingsScreenViewModelType.Context { get }
}
@@ -0,0 +1,99 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Compound
import SwiftUI
struct LocationSharingSettingsScreen: View {
static let measurementFormatter = {
let formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
formatter.unitStyle = .short
return formatter
}()
@Bindable var context: LocationSharingSettingsScreenViewModel.Context
var body: some View {
Form {
liveLocationSection
}
.compoundList()
.navigationTitle(L10n.commonLocationSharing)
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
private var liveLocationSection: some View {
let binding = Binding(get: {
Double(context.liveLocationMinimumDistanceUpdate)
}, set: { newValue in
context.liveLocationMinimumDistanceUpdate = Int(newValue)
})
Section {
ListRow(kind: .custom {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.screenAdvancedSettingsLiveLocationUpdateDistance(context.liveLocationMinimumDistanceUpdate))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textPrimary)
// The internal hidden label of the slider will read voice over
.accessibilityHidden(true)
Slider(value: binding, in: 1...100) {
Text(L10n.screenAdvancedSettingsLiveLocationUpdateDistance(context.liveLocationMinimumDistanceUpdate))
} minimumValueLabel: {
Text(Self.measurementFormatter.string(from: .init(value: 1,
unit: UnitLength.meters)))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textSecondary)
.padding(.trailing, 15)
} maximumValueLabel: {
Text(Self.measurementFormatter.string(from: .init(value: 100,
unit: UnitLength.meters)))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textSecondary)
.padding(.leading, 15)
}
.tint(.compound.iconAccentPrimary)
}
.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()
}
}
}
// MARK: - Previews
struct LocationSharingSettingsScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = LocationSharingSettingsScreenViewModel(userSettings: .mock())
static var previews: some View {
ElementNavigationStack {
LocationSharingSettingsScreen(context: viewModel.context)
}
}
}
private extension TimelineMediaVisibility {
static var items: [(title: String, tag: TimelineMediaVisibility)] {
[(title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysHide, tag: .never),
(title: L10n.screenAdvancedSettingsShowMediaTimelinePrivateRooms, tag: .privateOnly),
(title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysShow, tag: .always)]
}
}