mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Send pin-drop location (#1179)
* add location sharing action in room, and open location sharing screen * add pin location sharing * fix asset, add tests for location viewModel, add send location request * fix map zoom level, fix assets for location * add feature flag for location sharing * hide attribution button
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
enum LocationSharingViewError: Error, Hashable {
|
||||
@@ -21,13 +22,23 @@ enum LocationSharingViewError: Error, Hashable {
|
||||
case mapError(MapLibreError)
|
||||
}
|
||||
|
||||
enum StaticLocationScreenViewModelAction { }
|
||||
enum StaticLocationScreenViewModelAction {
|
||||
case close
|
||||
case sendLocation(GeoURI)
|
||||
}
|
||||
|
||||
struct StaticLocationScreenViewState: BindableState {
|
||||
/// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location
|
||||
var isPinDropSharing = true
|
||||
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
var showsUserLocationMode: ShowUserLocationMode = .hide
|
||||
|
||||
var bindings = StaticLocationScreenBindings()
|
||||
}
|
||||
|
||||
struct StaticLocationScreenBindings {
|
||||
var mapCenterLocation: CLLocationCoordinate2D?
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var mapError: MapLibreError? {
|
||||
get {
|
||||
@@ -45,4 +56,8 @@ struct StaticLocationScreenBindings {
|
||||
var alertInfo: AlertInfo<LocationSharingViewError>?
|
||||
}
|
||||
|
||||
enum StaticLocationScreenViewAction { }
|
||||
enum StaticLocationScreenViewAction {
|
||||
case close
|
||||
case selectLocation
|
||||
case userDidPan
|
||||
}
|
||||
|
||||
@@ -19,12 +19,22 @@ import SwiftUI
|
||||
|
||||
struct StaticLocationScreenCoordinatorParameters { }
|
||||
|
||||
enum StaticLocationScreenCoordinatorAction { }
|
||||
enum StaticLocationScreenCoordinatorAction {
|
||||
case close
|
||||
case selectedLocation(GeoURI)
|
||||
}
|
||||
|
||||
final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
let parameters: StaticLocationScreenCoordinatorParameters
|
||||
let viewModel: StaticLocationScreenViewModelProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<StaticLocationScreenCoordinatorAction, Never> = .init()
|
||||
private var cancellables: Set<AnyCancellable> = .init()
|
||||
|
||||
var actions: AnyPublisher<StaticLocationScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: StaticLocationScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
@@ -33,6 +43,19 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
viewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
case .sendLocation(let geoURI):
|
||||
actionsSubject.send(.selectedLocation(geoURI))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(StaticLocationScreen(context: viewModel.context))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
typealias StaticLocationScreenViewModelType = StateStoreViewModel<StaticLocationScreenViewState, StaticLocationScreenViewModelAction>
|
||||
typealias StaticLocationScreenViewModelType = StateStoreViewModel<StaticLocationScreenViewState, StaticLocationScreenViewAction>
|
||||
|
||||
class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLocationScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<StaticLocationScreenViewModelAction, Never> = .init()
|
||||
@@ -29,4 +29,17 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
|
||||
init() {
|
||||
super.init(initialViewState: StaticLocationScreenViewState())
|
||||
}
|
||||
|
||||
override func process(viewAction: StaticLocationScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
case .selectLocation:
|
||||
guard let coordinate = state.bindings.mapCenterLocation else { return }
|
||||
actionsSubject.send(.sendLocation(.init(coordinate: coordinate)))
|
||||
case .userDidPan:
|
||||
state.showsUserLocationMode = .hide
|
||||
state.isPinDropSharing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,18 +22,62 @@ struct StaticLocationScreen: View {
|
||||
private let builder = MapTilerStyleBuilder(appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
mapView
|
||||
.ignoresSafeArea(.all, edges: [.bottom])
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert(item: $context.alertInfo)
|
||||
mapView
|
||||
.ignoresSafeArea(.all, edges: .horizontal)
|
||||
.navigationTitle(L10n.screenShareLocationTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
|
||||
private var mapView: some View {
|
||||
ZStack(alignment: .center) {
|
||||
MapLibreMapView(builder: builder,
|
||||
showsUserLocationMode: .hide,
|
||||
error: $context.mapError,
|
||||
mapCenterCoordinate: $context.mapCenterLocation,
|
||||
userDidPan: {
|
||||
context.send(viewAction: .userDidPan)
|
||||
})
|
||||
if context.viewState.isPinDropSharing {
|
||||
LocationMarkerView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mapView: MapLibreMapView {
|
||||
MapLibreMapView(builder: builder,
|
||||
showsUserLocationMode: .follow,
|
||||
error: $context.mapError)
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
closeButton
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
shareLocationButton
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ScaledMetric private var shareMarkerSize: CGFloat = 28
|
||||
private var shareLocationButton: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectLocation)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(asset: Asset.Images.locationMarker)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: shareMarkerSize, height: shareMarkerSize)
|
||||
Text(context.viewState.isPinDropSharing ? L10n.screenShareThisLocationAction : L10n.screenShareMyLocationAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(L10n.actionCancel, action: close)
|
||||
}
|
||||
|
||||
private func close() {
|
||||
context.send(viewAction: .close)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ enum RoomScreenCoordinatorAction {
|
||||
case presentMediaUploadPicker(MediaPickerScreenSource)
|
||||
case presentMediaUploadPreviewScreen(URL)
|
||||
case presentRoomDetails
|
||||
case presentLocationPicker
|
||||
case presentEmojiPicker(itemID: String)
|
||||
case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
|
||||
case presentMessageForwarding(itemID: String)
|
||||
@@ -78,6 +79,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentMediaUploadPicker(.photoLibrary))
|
||||
case .displayDocumentPicker:
|
||||
actionsSubject.send(.presentMediaUploadPicker(.documents))
|
||||
case .displayLocationPicker:
|
||||
actionsSubject.send(.presentLocationPicker)
|
||||
case .displayMediaUploadPreviewScreen(let url):
|
||||
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
|
||||
case .displayRoomMemberDetails(let member):
|
||||
|
||||
@@ -26,6 +26,7 @@ enum RoomScreenViewModelAction {
|
||||
case displayCameraPicker
|
||||
case displayMediaPicker
|
||||
case displayDocumentPicker
|
||||
case displayLocationPicker
|
||||
case displayMediaUploadPreviewScreen(url: URL)
|
||||
case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
|
||||
case displayMessageForwarding(itemID: String)
|
||||
@@ -68,6 +69,7 @@ enum RoomScreenViewAction {
|
||||
case displayCameraPicker
|
||||
case displayMediaPicker
|
||||
case displayDocumentPicker
|
||||
case displayLocationPicker
|
||||
|
||||
case handlePasteOrDrop(provider: NSItemProvider)
|
||||
case tappedOnUser(userID: String)
|
||||
|
||||
@@ -109,6 +109,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
callback?(.displayMediaPicker)
|
||||
case .displayDocumentPicker:
|
||||
callback?(.displayDocumentPicker)
|
||||
case .displayLocationPicker:
|
||||
callback?(.displayLocationPicker)
|
||||
case .handlePasteOrDrop(let provider):
|
||||
handlePasteOrDrop(provider)
|
||||
case .tappedOnUser(userID: let userID):
|
||||
|
||||
@@ -66,7 +66,7 @@ struct TimelineReplyView: View {
|
||||
ReplyView(sender: sender,
|
||||
plainBody: L10n.commonSharedLocation,
|
||||
formattedBody: nil,
|
||||
icon: .init(kind: .icon(Asset.Images.locationPin.name), cornerRadii: iconCornerRadii))
|
||||
icon: .init(kind: .icon(Asset.Images.locationMarker.name), cornerRadii: iconCornerRadii))
|
||||
}
|
||||
default:
|
||||
LoadingReplyView()
|
||||
|
||||
@@ -37,21 +37,30 @@ struct RoomAttachmentPicker: View {
|
||||
context.showAttachmentPopover = false
|
||||
context.send(viewAction: .displayMediaPicker)
|
||||
} label: {
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceGallery, systemImageName: "photo.fill")
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceGallery, icon: Image(systemName: "photo.fill"))
|
||||
}
|
||||
|
||||
Button {
|
||||
context.showAttachmentPopover = false
|
||||
context.send(viewAction: .displayDocumentPicker)
|
||||
} label: {
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceFiles, systemImageName: "paperclip")
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceFiles, icon: Image(systemName: "paperclip"))
|
||||
}
|
||||
|
||||
Button {
|
||||
context.showAttachmentPopover = false
|
||||
context.send(viewAction: .displayCameraPicker)
|
||||
} label: {
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, systemImageName: "camera.fill")
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, icon: Image(systemName: "camera.fill"))
|
||||
}
|
||||
|
||||
if ServiceLocator.shared.settings.shareLocationEnabled {
|
||||
Button {
|
||||
context.showAttachmentPopover = false
|
||||
context.send(viewAction: .displayLocationPicker)
|
||||
} label: {
|
||||
PickerLabel(title: L10n.screenRoomAttachmentSourceLocation, icon: Image(asset: Asset.Images.locationPin))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, isPresented ? 20 : 0)
|
||||
@@ -73,14 +82,23 @@ struct RoomAttachmentPicker: View {
|
||||
|
||||
private struct PickerLabel: View {
|
||||
let title: String
|
||||
let systemImageName: String
|
||||
let icon: Image
|
||||
|
||||
init(title: String, icon: Image) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Label(title, systemImage: systemImageName)
|
||||
.labelStyle(FixedIconSizeLabelStyle())
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
Label {
|
||||
Text(title)
|
||||
} icon: {
|
||||
icon
|
||||
}
|
||||
.labelStyle(FixedIconSizeLabelStyle())
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ struct LocationRoomTimelineView: View {
|
||||
if let geoURI = timelineItem.content.geoURI {
|
||||
let mapSize: CGSize = .init(width: 292, height: 188)
|
||||
MapLibreStaticMapView(geoURI: geoURI, size: mapSize) {
|
||||
LocationPinView()
|
||||
LocationMarkerView()
|
||||
}
|
||||
.frame(width: mapSize.width, height: mapSize.height)
|
||||
} else {
|
||||
|
||||
+2
@@ -30,6 +30,7 @@ struct DeveloperOptionsScreenViewStateBindings {
|
||||
var readReceiptsEnabled: Bool
|
||||
var isEncryptionSyncEnabled: Bool
|
||||
var locationEventsEnabled: Bool
|
||||
var shareLocationEnabled: Bool
|
||||
}
|
||||
|
||||
enum DeveloperOptionsScreenViewAction {
|
||||
@@ -38,5 +39,6 @@ enum DeveloperOptionsScreenViewAction {
|
||||
case changedReadReceiptsEnabled
|
||||
case changedIsEncryptionSyncEnabled
|
||||
case changedLocationEventsEnabled
|
||||
case changedShareLocationEnabled
|
||||
case clearCache
|
||||
}
|
||||
|
||||
+4
-1
@@ -30,7 +30,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
||||
userSuggestionsEnabled: appSettings.userSuggestionsEnabled,
|
||||
readReceiptsEnabled: appSettings.readReceiptsEnabled,
|
||||
isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled,
|
||||
locationEventsEnabled: appSettings.locationEventsEnabled)
|
||||
locationEventsEnabled: appSettings.locationEventsEnabled,
|
||||
shareLocationEnabled: appSettings.shareLocationEnabled)
|
||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
@@ -52,6 +53,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
||||
appSettings.isEncryptionSyncEnabled = state.bindings.isEncryptionSyncEnabled
|
||||
case .changedLocationEventsEnabled:
|
||||
appSettings.locationEventsEnabled = state.bindings.locationEventsEnabled
|
||||
case .changedShareLocationEnabled:
|
||||
appSettings.shareLocationEnabled = state.bindings.shareLocationEnabled
|
||||
case .clearCache:
|
||||
callback?(.clearCache)
|
||||
}
|
||||
|
||||
+7
@@ -59,6 +59,13 @@ struct DeveloperOptionsScreen: View {
|
||||
.onChange(of: context.locationEventsEnabled) { _ in
|
||||
context.send(viewAction: .changedLocationEventsEnabled)
|
||||
}
|
||||
|
||||
Toggle(isOn: $context.shareLocationEnabled) {
|
||||
Text("Show share location action")
|
||||
}
|
||||
.onChange(of: context.shareLocationEnabled) { _ in
|
||||
context.send(viewAction: .changedShareLocationEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
Reference in New Issue
Block a user