mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Send current user location (#1272)
* add new asset for location, add logic for sharing user current location * work on default zoom and auth for location * add attribution button * fix attribution button position, fix isSharingUserLocation * Add NSLocationWhenInUseUsageDescription back * Fix initial animation * change map logo and attribution position * Add annotation just in the setup * add errors and alert for map vies * location unit test * add unit test for location sharing * add changelog * Update ElementX/Sources/Other/MapLibre/MapLibreMapView.swift Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com> * add comments and minor parameters refactor * Update ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com> * Update ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com> * Sort assets * Cleanup * Cleanup * Fix error localizations * Fix tests --------- Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com> Co-authored-by: Mauro <34335419+Velin92@users.noreply.github.com>
This commit is contained in:
@@ -515,7 +515,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
_ = await self.roomProxy?.sendLocation(body: geoURI.bodyMessage,
|
||||
geoURI: geoURI,
|
||||
description: nil,
|
||||
zoomLevel: nil,
|
||||
zoomLevel: 15,
|
||||
assetType: isUserLocation ? .sender : .pin)
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ internal enum Asset {
|
||||
internal static let launchLogo = ImageAsset(name: "images/launch-logo")
|
||||
internal static let locationMarker = ImageAsset(name: "images/location-marker")
|
||||
internal static let locationPin = ImageAsset(name: "images/location-pin")
|
||||
internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full")
|
||||
internal static let locationPointer = ImageAsset(name: "images/location-pointer")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message")
|
||||
internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient")
|
||||
}
|
||||
|
||||
@@ -308,14 +308,20 @@ public enum L10n {
|
||||
public static var emojiPickerCategorySymbols: String { return L10n.tr("Localizable", "emoji_picker_category_symbols") }
|
||||
/// Failed creating the permalink
|
||||
public static var errorFailedCreatingThePermalink: String { return L10n.tr("Localizable", "error_failed_creating_the_permalink") }
|
||||
/// Element could not load the map. Please try again later.
|
||||
public static var errorFailedLoadingMap: String { return L10n.tr("Localizable", "error_failed_loading_map") }
|
||||
/// %1$@ could not load the map. Please try again later.
|
||||
public static func errorFailedLoadingMap(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "error_failed_loading_map", String(describing: p1))
|
||||
}
|
||||
/// Failed loading messages
|
||||
public static var errorFailedLoadingMessages: String { return L10n.tr("Localizable", "error_failed_loading_messages") }
|
||||
/// Element could not access your location. Please try again later.
|
||||
public static var errorFailedLocatingUser: String { return L10n.tr("Localizable", "error_failed_locating_user") }
|
||||
/// Element does not have permission to access your location. You can enable access in Settings > Location
|
||||
public static var errorMissingLocationAuth: String { return L10n.tr("Localizable", "error_missing_location_auth") }
|
||||
/// %1$@ could not access your location. Please try again later.
|
||||
public static func errorFailedLocatingUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "error_failed_locating_user", String(describing: p1))
|
||||
}
|
||||
/// %1$@ does not have permission to access your location. You can enable access in Settings > Location
|
||||
public static func errorMissingLocationAuth(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "error_missing_location_auth", String(describing: p1))
|
||||
}
|
||||
/// No compatible app was found to handle this action.
|
||||
public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") }
|
||||
/// Some messages have not been sent
|
||||
@@ -780,6 +786,14 @@ public enum L10n {
|
||||
public static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") }
|
||||
/// Your message failed to send
|
||||
public static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") }
|
||||
/// Add emoji
|
||||
public static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") }
|
||||
/// Show less
|
||||
public static var screenRoomTimelineLessReactions: String { return L10n.tr("Localizable", "screen_room_timeline_less_reactions") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
public static func screenRoomTimelineMoreReactions(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_timeline_more_reactions", p1)
|
||||
}
|
||||
/// Create a new conversation or room
|
||||
public static var screenRoomlistA11yCreateMessage: String { return L10n.tr("Localizable", "screen_roomlist_a11y_create_message") }
|
||||
/// All Chats
|
||||
|
||||
@@ -20,15 +20,20 @@ import SwiftUI
|
||||
|
||||
struct MapLibreMapView: UIViewRepresentable {
|
||||
struct Options {
|
||||
/// The initial zoom level
|
||||
/// the final zoom level used when the first user location emit
|
||||
let zoomLevel: Double
|
||||
/// The initial zoom level used when the map it firstly loaded and the user location is not yet available, in case of annotations this property is not being used
|
||||
let initialZoomLevel: Double
|
||||
|
||||
/// The initial map center
|
||||
let mapCenter: CLLocationCoordinate2D?
|
||||
let mapCenter: CLLocationCoordinate2D
|
||||
|
||||
/// Map annotations
|
||||
let annotations: [LocationAnnotation]
|
||||
|
||||
init(zoomLevel: Double, mapCenter: CLLocationCoordinate2D? = nil, annotations: [LocationAnnotation] = []) {
|
||||
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) {
|
||||
self.zoomLevel = zoomLevel
|
||||
self.initialZoomLevel = initialZoomLevel
|
||||
self.mapCenter = mapCenter
|
||||
self.annotations = annotations
|
||||
}
|
||||
@@ -43,14 +48,16 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
let options: Options
|
||||
|
||||
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
var showsUserLocationMode: ShowUserLocationMode = .hide
|
||||
@Binding var showsUserLocationMode: ShowUserLocationMode
|
||||
|
||||
/// Bind view errors if any
|
||||
let error: Binding<MapLibreError?>
|
||||
@Binding var error: MapLibreError?
|
||||
|
||||
/// Coordinate of the center of the map
|
||||
@Binding var mapCenterCoordinate: CLLocationCoordinate2D?
|
||||
|
||||
@Binding var isLocationAuthorized: Bool?
|
||||
|
||||
/// Called when the user pan on the map
|
||||
var userDidPan: (() -> Void)?
|
||||
|
||||
@@ -59,23 +66,12 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> MGLMapView {
|
||||
let mapView = makeMapView()
|
||||
mapView.delegate = context.coordinator
|
||||
let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan))
|
||||
panGesture.delegate = context.coordinator
|
||||
mapView.addGestureRecognizer(panGesture)
|
||||
setupMap(mapView: mapView, with: options)
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MGLMapView, context: Context) {
|
||||
mapView.removeAllAnnotations()
|
||||
mapView.addAnnotations(options.annotations)
|
||||
|
||||
if colorScheme == .dark {
|
||||
mapView.styleURL = builder.dynamicMapURL(for: .dark)
|
||||
} else {
|
||||
mapView.styleURL = builder.dynamicMapURL(for: .light)
|
||||
}
|
||||
|
||||
mapView.styleURL = builder.dynamicMapURL(for: .init(colorScheme))
|
||||
showUserLocation(in: mapView)
|
||||
}
|
||||
|
||||
@@ -86,32 +82,36 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
// MARK: - Private
|
||||
|
||||
private func setupMap(mapView: MGLMapView, with options: Options) {
|
||||
mapView.zoomLevel = options.zoomLevel
|
||||
if let mapCenter = options.mapCenter {
|
||||
mapView.centerCoordinate = mapCenter
|
||||
}
|
||||
mapView.addAnnotations(options.annotations)
|
||||
mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel
|
||||
mapView.centerCoordinate = options.mapCenter
|
||||
}
|
||||
|
||||
private func makeMapView() -> MGLMapView {
|
||||
let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light))
|
||||
|
||||
showUserLocation(in: mapView)
|
||||
mapView.attributionButton.isHidden = true
|
||||
|
||||
mapView.logoViewPosition = .topLeft
|
||||
mapView.attributionButtonPosition = .topLeft
|
||||
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
|
||||
return mapView
|
||||
}
|
||||
|
||||
private func showUserLocation(in mapView: MGLMapView) {
|
||||
switch showsUserLocationMode {
|
||||
case .showAndFollow:
|
||||
mapView.showsUserLocation = true
|
||||
switch (showsUserLocationMode, options.annotations) {
|
||||
case (.showAndFollow, _):
|
||||
mapView.userTrackingMode = .follow
|
||||
case .show:
|
||||
case (.show, let annotations) where !annotations.isEmpty:
|
||||
/** in the show mode, if there are annotations, we check the authorizationStatus,
|
||||
if it's not determined, we wont prompt the user with a request for permissions,
|
||||
because he should be able to see the annotations without sharing his location informations
|
||||
**/
|
||||
guard mapView.locationManager.authorizationStatus != .notDetermined else { return }
|
||||
fallthrough
|
||||
case (.show, _):
|
||||
mapView.showsUserLocation = true
|
||||
mapView.userTrackingMode = .none
|
||||
case .hide:
|
||||
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
|
||||
case (.hide, _):
|
||||
mapView.showsUserLocation = false
|
||||
mapView.userTrackingMode = .none
|
||||
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,11 +119,13 @@ struct MapLibreMapView: UIViewRepresentable {
|
||||
// MARK: - Coordinator
|
||||
|
||||
extension MapLibreMapView {
|
||||
class Coordinator: NSObject, MGLMapViewDelegate, UIGestureRecognizerDelegate {
|
||||
class Coordinator: NSObject, MGLMapViewDelegate {
|
||||
// MARK: - Properties
|
||||
|
||||
var mapLibreView: MapLibreMapView
|
||||
|
||||
private var previousUserLocation: MGLUserLocation?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(_ mapLibreView: MapLibreMapView) {
|
||||
@@ -140,20 +142,30 @@ extension MapLibreMapView {
|
||||
}
|
||||
|
||||
func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) {
|
||||
mapLibreView.error.wrappedValue = .failedLoadingMap
|
||||
mapLibreView.error = .failedLoadingMap
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { }
|
||||
func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) {
|
||||
guard let userLocation else { return }
|
||||
|
||||
if previousUserLocation == nil, mapLibreView.options.annotations.isEmpty {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.options.zoomLevel, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
previousUserLocation = userLocation
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) {
|
||||
guard mapView.showsUserLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
switch manager.authorizationStatus {
|
||||
case .denied, .restricted:
|
||||
mapLibreView.error.wrappedValue = .invalidLocationAuthorization
|
||||
default:
|
||||
mapLibreView.isLocationAuthorized = false
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
mapLibreView.isLocationAuthorized = true
|
||||
case .notDetermined:
|
||||
mapLibreView.isLocationAuthorized = nil
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -171,15 +183,25 @@ extension MapLibreMapView {
|
||||
false
|
||||
}
|
||||
|
||||
// MARK: UIGestureRecognizer
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
gestureRecognizer is UIPanGestureRecognizer
|
||||
}
|
||||
|
||||
@objc
|
||||
func didPan() {
|
||||
mapLibreView.userDidPan?()
|
||||
func mapView(_ mapView: MGLMapView, shouldChangeFrom oldCamera: MGLMapCamera, to newCamera: MGLMapCamera, reason: MGLCameraChangeReason) -> Bool {
|
||||
// we send the userDidPan event only for the reasons that actually will change the map center, and not zoom only / rotations only events.
|
||||
switch reason {
|
||||
case .gesturePan,
|
||||
.gesturePinch,
|
||||
.gestureRotate:
|
||||
mapLibreView.userDidPan?()
|
||||
case .gestureOneFingerZoom,
|
||||
.gestureTilt,
|
||||
.gestureZoomIn,
|
||||
.gestureZoomOut,
|
||||
.programmatic,
|
||||
.resetNorth,
|
||||
.transitionCancelled:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,3 +216,16 @@ private extension MGLMapView {
|
||||
removeAnnotations(annotations)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MapTilerStyle {
|
||||
init(_ colorScheme: ColorScheme) {
|
||||
switch colorScheme {
|
||||
case .light:
|
||||
self = .light
|
||||
case .dark:
|
||||
self = .dark
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ enum MapTilerStyle {
|
||||
enum MapLibreError: Error {
|
||||
case failedLoadingMap
|
||||
case failedLocatingUser
|
||||
case invalidLocationAuthorization
|
||||
}
|
||||
|
||||
enum MapTilerAttributionPlacement: String {
|
||||
|
||||
@@ -18,12 +18,13 @@ import CoreLocation
|
||||
import Foundation
|
||||
|
||||
enum LocationSharingViewError: Error, Hashable {
|
||||
case failedSharingLocation
|
||||
case missingAuthorization
|
||||
case mapError(MapLibreError)
|
||||
}
|
||||
|
||||
enum StaticLocationScreenViewModelAction {
|
||||
case close
|
||||
case openSystemSettings
|
||||
case sendLocation(GeoURI, isUserLocation: Bool)
|
||||
}
|
||||
|
||||
@@ -33,47 +34,36 @@ enum StaticLocationInteractionMode: Hashable {
|
||||
}
|
||||
|
||||
struct StaticLocationScreenViewState: BindableState {
|
||||
init(interactionMode: StaticLocationInteractionMode, isSharingUserLocation: Bool = false, showsUserLocationMode: ShowUserLocationMode = .hide) {
|
||||
init(interactionMode: StaticLocationInteractionMode) {
|
||||
self.interactionMode = interactionMode
|
||||
self.isSharingUserLocation = isSharingUserLocation
|
||||
self.showsUserLocationMode = showsUserLocationMode
|
||||
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
bindings = .init()
|
||||
case .viewOnly(let geoURI, _):
|
||||
bindings = .init(mapCenterLocation: .init(latitude: geoURI.latitude, longitude: geoURI.longitude))
|
||||
bindings.showsUserLocationMode = .showAndFollow
|
||||
case .viewOnly:
|
||||
bindings.showsUserLocationMode = .show
|
||||
}
|
||||
}
|
||||
|
||||
let interactionMode: StaticLocationInteractionMode
|
||||
/// Indicates whether the user is sharing his current location
|
||||
var isSharingUserLocation: Bool
|
||||
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
var showsUserLocationMode: ShowUserLocationMode
|
||||
|
||||
var bindings = StaticLocationScreenBindings()
|
||||
|
||||
var showBottomToolbar: Bool {
|
||||
interactionMode == .picker
|
||||
var isSharingUserLocation: Bool {
|
||||
bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow
|
||||
}
|
||||
|
||||
var mapAnnotationCoordinate: CLLocationCoordinate2D? {
|
||||
|
||||
var bindings = StaticLocationScreenBindings(showsUserLocationMode: .hide)
|
||||
|
||||
var initialMapCenter: CLLocationCoordinate2D {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
return nil
|
||||
// middle point in Europe, to be used if the users location is not yet known
|
||||
return .init(latitude: 49.843, longitude: 9.902056)
|
||||
case .viewOnly(let geoURI, _):
|
||||
return .init(latitude: geoURI.latitude, longitude: geoURI.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
var isLocationPickerMode: Bool {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
return true
|
||||
case .viewOnly:
|
||||
return false
|
||||
}
|
||||
interactionMode == .picker
|
||||
}
|
||||
|
||||
var navigationTitle: String {
|
||||
@@ -95,9 +85,13 @@ struct StaticLocationScreenViewState: BindableState {
|
||||
}
|
||||
|
||||
var zoomLevel: Double {
|
||||
15.0
|
||||
}
|
||||
|
||||
var initialZoomLevel: Double {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
return 5.0
|
||||
return 2.7
|
||||
case .viewOnly:
|
||||
return 15.0
|
||||
}
|
||||
@@ -115,6 +109,10 @@ struct StaticLocationScreenViewState: BindableState {
|
||||
|
||||
struct StaticLocationScreenBindings {
|
||||
var mapCenterLocation: CLLocationCoordinate2D?
|
||||
|
||||
var showsUserLocationMode: ShowUserLocationMode
|
||||
|
||||
var isLocationAuthorized: Bool?
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var mapError: MapLibreError? {
|
||||
@@ -125,7 +123,7 @@ struct StaticLocationScreenBindings {
|
||||
return nil
|
||||
}
|
||||
set {
|
||||
alertInfo = newValue.map { AlertInfo(id: .mapError($0)) }
|
||||
alertInfo = newValue.map { AlertInfo(locationSharingViewError: .mapError($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,5 +136,33 @@ struct StaticLocationScreenBindings {
|
||||
enum StaticLocationScreenViewAction {
|
||||
case close
|
||||
case selectLocation
|
||||
case centerToUser
|
||||
case userDidPan
|
||||
}
|
||||
|
||||
extension AlertInfo where T == LocationSharingViewError {
|
||||
init(locationSharingViewError error: LocationSharingViewError,
|
||||
primaryButton: AlertButton = AlertButton(title: L10n.actionOk, action: nil),
|
||||
secondaryButton: AlertButton? = nil) {
|
||||
switch error {
|
||||
case .missingAuthorization:
|
||||
self.init(id: error,
|
||||
title: "",
|
||||
message: L10n.errorMissingLocationAuth(InfoPlistReader.main.bundleDisplayName),
|
||||
primaryButton: primaryButton,
|
||||
secondaryButton: secondaryButton)
|
||||
case .mapError(.failedLoadingMap):
|
||||
self.init(id: error,
|
||||
title: "",
|
||||
message: L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName),
|
||||
primaryButton: primaryButton,
|
||||
secondaryButton: secondaryButton)
|
||||
case .mapError(.failedLocatingUser):
|
||||
self.init(id: error,
|
||||
title: "",
|
||||
message: L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName),
|
||||
primaryButton: primaryButton,
|
||||
secondaryButton: secondaryButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
case .openSystemSettings:
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString),
|
||||
UIApplication.shared.canOpenURL(url) else {
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(url)
|
||||
case .sendLocation(let geoURI, let isUserLocation):
|
||||
actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation))
|
||||
}
|
||||
|
||||
@@ -38,8 +38,17 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
|
||||
guard let coordinate = state.bindings.mapCenterLocation else { return }
|
||||
actionsSubject.send(.sendLocation(.init(coordinate: coordinate), isUserLocation: state.isSharingUserLocation))
|
||||
case .userDidPan:
|
||||
state.showsUserLocationMode = .hide
|
||||
state.isSharingUserLocation = false
|
||||
state.bindings.showsUserLocationMode = .show
|
||||
case .centerToUser:
|
||||
switch state.bindings.isLocationAuthorized {
|
||||
case .some(true), .none:
|
||||
state.bindings.showsUserLocationMode = .showAndFollow
|
||||
case .some(false):
|
||||
let action: () -> Void = { [weak self] in self?.actionsSubject.send(.openSystemSettings) }
|
||||
state.bindings.alertInfo = .init(locationSharingViewError: .missingAuthorization,
|
||||
primaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.commonSettings, action: action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,17 +44,20 @@ struct StaticLocationScreen: View {
|
||||
ZStack(alignment: .center) {
|
||||
MapLibreMapView(builder: builder,
|
||||
options: mapOptions,
|
||||
showsUserLocationMode: .hide,
|
||||
showsUserLocationMode: $context.showsUserLocationMode,
|
||||
error: $context.mapError,
|
||||
mapCenterCoordinate: $context.mapCenterLocation,
|
||||
userDidPan: {
|
||||
context.send(viewAction: .userDidPan)
|
||||
})
|
||||
isLocationAuthorized: $context.isLocationAuthorized) {
|
||||
context.send(viewAction: .userDidPan)
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
|
||||
if context.viewState.isLocationPickerMode {
|
||||
LocationMarkerView()
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
centerToUserLocationButton
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -72,7 +75,7 @@ struct StaticLocationScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
if context.viewState.showBottomToolbar {
|
||||
if context.viewState.isLocationPickerMode {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
selectLocationButton
|
||||
Spacer()
|
||||
@@ -81,19 +84,22 @@ struct StaticLocationScreen: View {
|
||||
}
|
||||
|
||||
private var mapOptions: MapLibreMapView.Options {
|
||||
guard let coordinate = context.viewState.mapAnnotationCoordinate else {
|
||||
return .init(zoomLevel: context.viewState.zoomLevel)
|
||||
var annotations: [LocationAnnotation] = []
|
||||
if context.viewState.isLocationPickerMode == false {
|
||||
let annotation = LocationAnnotation(coordinate: context.viewState.initialMapCenter, anchorPoint: .bottomCenter) {
|
||||
LocationMarkerView()
|
||||
}
|
||||
annotations.append(annotation)
|
||||
}
|
||||
|
||||
return .init(zoomLevel: context.viewState.zoomLevel,
|
||||
mapCenter: coordinate,
|
||||
annotations: [LocationAnnotation(coordinate: coordinate, anchorPoint: .bottomCenter) {
|
||||
LocationMarkerView()
|
||||
}])
|
||||
initialZoomLevel: context.viewState.initialZoomLevel,
|
||||
mapCenter: context.viewState.initialMapCenter,
|
||||
annotations: annotations)
|
||||
}
|
||||
|
||||
private var mapSafeAreaEdges: Edge.Set {
|
||||
context.viewState.showBottomToolbar ? .horizontal : [.horizontal, .bottom]
|
||||
context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom]
|
||||
}
|
||||
|
||||
@ScaledMetric private var shareMarkerSize: CGFloat = 28
|
||||
@@ -111,6 +117,15 @@ struct StaticLocationScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var centerToUserLocationButton: some View {
|
||||
Button {
|
||||
context.send(viewAction: .centerToUser)
|
||||
} label: {
|
||||
Image(asset: context.viewState.isSharingUserLocation ? Asset.Images.locationPointerFull : Asset.Images.locationPointer)
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .close)
|
||||
@@ -127,14 +142,13 @@ struct StaticLocationScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var shareSheet: some View {
|
||||
if let location = context.viewState.mapAnnotationCoordinate {
|
||||
let locationDescription = context.viewState.locationDescription
|
||||
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)],
|
||||
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) })
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.hidden)
|
||||
}
|
||||
let location = context.viewState.initialMapCenter
|
||||
let locationDescription = context.viewState.locationDescription
|
||||
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, locationDescription: locationDescription)],
|
||||
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, locationDescription: locationDescription) })
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity function_body_length
|
||||
private func updateTimelineItems() {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
var canBackPaginate = true
|
||||
|
||||
Reference in New Issue
Block a user