Merge branch 'develop' into steve/5903_lls_start

# Conflicts:
#	Riot/Modules/Room/RoomViewController.h
#	RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift
#	RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift
#	RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift
This commit is contained in:
SBiOSoftWhare
2022-04-08 11:09:06 +02:00
221 changed files with 8846 additions and 1792 deletions
@@ -17,12 +17,44 @@
import Foundation
import UIKit
import SwiftUI
import MatrixSDK
struct LocationSharingCoordinatorParameters {
let roomDataSource: MXKRoomDataSource
let mediaManager: MXMediaManager
let avatarData: AvatarInputProtocol
let location: CLLocationCoordinate2D?
let coordinateType: MXEventAssetType
}
// Map between type from MatrixSDK and type from SwiftUI target, as we don't want
// to add the SDK as a dependency to it. We need to translate from one to the other on this level.
extension MXEventAssetType {
func locationSharingCoordinateType() -> LocationSharingCoordinateType {
let coordinateType: LocationSharingCoordinateType
switch self {
case .user, .generic:
coordinateType = .user
case .pin:
coordinateType = .pin
@unknown default:
coordinateType = .user
}
return coordinateType
}
}
extension LocationSharingCoordinateType {
func eventAssetType() -> MXEventAssetType {
let eventAssetType: MXEventAssetType
switch self {
case .user:
eventAssetType = .user
case .pin:
eventAssetType = .pin
}
return eventAssetType
}
}
final class LocationSharingCoordinator: Coordinator, Presentable {
@@ -50,6 +82,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
let viewModel = LocationSharingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
avatarData: parameters.avatarData,
location: parameters.location,
coordinateType: parameters.coordinateType.locationSharingCoordinateType(),
isLiveLocationSharingEnabled: BuildSettings.liveLocationSharingEnabled)
let view = LocationSharingView(context: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
@@ -71,13 +104,26 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
switch result {
case .cancel:
self.completion?()
case .share(let latitude, let longitude):
case .share(let latitude, let longitude, let coordinateType):
// Show share sheet on existing location display
if let location = self.parameters.location {
self.presentShareLocationActivity(with: location)
} else {
self.shareStaticLocation(latitude: latitude, longitude: longitude)
self.locationSharingHostingController.present(Self.shareLocationActivityController(location), animated: true)
return
}
self.locationSharingViewModel.startLoading()
self.parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil, coordinateType: coordinateType.eventAssetType()) { [weak self] _ in
guard let self = self else { return }
self.locationSharingViewModel.stopLoading()
self.completion?()
} failure: { [weak self] error in
guard let self = self else { return }
MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))")
self.locationSharingViewModel.stopLoading(error: .locationSharingError)
}
case .shareLiveLocation(let timeout):
@@ -17,22 +17,35 @@
import Foundation
import Mapbox
class UserLocationAnnotation: NSObject, MGLAnnotation {
class LocationAnnotation: NSObject, MGLAnnotation {
// MARK: - Properties
let avatarData: AvatarInputProtocol
let coordinate: CLLocationCoordinate2D
// MARK: - Setup
init(avatarData: AvatarInputProtocol,
coordinate: CLLocationCoordinate2D) {
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
self.avatarData = avatarData
super.init()
}
}
class PinLocationAnnotation: LocationAnnotation {}
class UserLocationAnnotation: LocationAnnotation {
// MARK: - Properties
let avatarData: AvatarInputProtocol
// MARK: - Setup
init(avatarData: AvatarInputProtocol,
coordinate: CLLocationCoordinate2D) {
self.avatarData = avatarData
super.init(coordinate: coordinate)
}
}
@@ -19,15 +19,22 @@ import SwiftUI
import Combine
import CoreLocation
// This is the equivalent of MXEventAssetType in the MatrixSDK
enum LocationSharingCoordinateType {
case user
case pin
}
enum LocationSharingViewAction {
case cancel
case share
case shareLiveLocation
case sharePinLocation
case goToUserLocation
}
enum LocationSharingViewModelResult {
case cancel
case share(latitude: Double, longitude: Double)
case share(latitude: Double, longitude: Double, coordinateType: LocationSharingCoordinateType)
case shareLiveLocation(timeout: TimeInterval)
}
@@ -47,17 +54,19 @@ struct LocationSharingViewState: BindableState {
/// Current user avatarData
let userAvatarData: AvatarInputProtocol
/// User map annotation to display existing location
let userAnnotation: UserLocationAnnotation?
/// Shared annotation to display existing location
let sharedAnnotation: LocationAnnotation?
/// Map annotations to display on map
var annotations: [UserLocationAnnotation]
var annotations: [LocationAnnotation]
/// Map annotation to focus on
var highlightedAnnotation: UserLocationAnnotation?
var highlightedAnnotation: LocationAnnotation?
/// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location
var isPinDropSharing: Bool = false
var isPinDropSharing: Bool {
return bindings.pinLocation != nil
}
var showLoadingIndicator: Bool = false
@@ -72,7 +81,7 @@ struct LocationSharingViewState: BindableState {
}
var displayExistingLocation: Bool {
return userAnnotation != nil
return sharedAnnotation != nil
}
var shareButtonEnabled: Bool {
@@ -87,6 +96,7 @@ struct LocationSharingViewState: BindableState {
struct LocationSharingViewStateBindings {
var alertInfo: AlertInfo<LocationSharingAlertType>?
var userLocation: CLLocationCoordinate2D?
var pinLocation: CLLocationCoordinate2D?
}
enum LocationSharingAlertType {
@@ -38,6 +38,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
let viewModel = LocationSharingViewModel(mapStyleURL: mapStyleURL,
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
location: location,
coordinateType: .user,
isLiveLocationSharingEnabled: true)
return ([viewModel],
AnyView(LocationSharingView(context: viewModel.context)
@@ -41,21 +41,27 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
// MARK: - Setup
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil, isLiveLocationSharingEnabled: Bool = false) {
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil, coordinateType: LocationSharingCoordinateType, isLiveLocationSharingEnabled: Bool = false) {
var userAnnotation: UserLocationAnnotation?
var annotations: [UserLocationAnnotation] = []
var highlightedAnnotation: UserLocationAnnotation?
var sharedAnnotation: LocationAnnotation?
var annotations: [LocationAnnotation] = []
var highlightedAnnotation: LocationAnnotation?
var showsUserLocation: Bool = false
// Displaying an existing location
if let userCoordinate = location {
let userLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: userCoordinate)
if let sharedCoordinate = location {
let sharedLocationAnnotation: LocationAnnotation
switch coordinateType {
case .user:
sharedLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: sharedCoordinate)
case .pin:
sharedLocationAnnotation = PinLocationAnnotation(coordinate: sharedCoordinate)
}
annotations.append(userLocationAnnotation)
highlightedAnnotation = userLocationAnnotation
annotations.append(sharedLocationAnnotation)
highlightedAnnotation = sharedLocationAnnotation
userAnnotation = userLocationAnnotation
sharedAnnotation = sharedLocationAnnotation
} else {
// Share current location
showsUserLocation = true
@@ -63,7 +69,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL,
userAvatarData: avatarData,
userAnnotation: userAnnotation,
sharedAnnotation: sharedAnnotation,
annotations: annotations,
highlightedAnnotation: highlightedAnnotation,
showsUserLocation: showsUserLocation,
@@ -85,8 +91,8 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
completion?(.cancel)
case .share:
// Share existing location
if let location = state.userAnnotation?.coordinate {
completion?(.share(latitude: location.latitude, longitude: location.longitude))
if let location = state.sharedAnnotation?.coordinate {
completion?(.share(latitude: location.latitude, longitude: location.longitude, coordinateType: .user))
return
}
@@ -96,7 +102,16 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
return
}
completion?(.share(latitude: location.latitude, longitude: location.longitude))
completion?(.share(latitude: location.latitude, longitude: location.longitude, coordinateType: .user))
case .sharePinLocation:
guard let pinLocation = state.bindings.pinLocation else {
processError(.failedLocatingUser)
return
}
completion?(.share(latitude: pinLocation.latitude, longitude: pinLocation.longitude, coordinateType: .pin))
case .goToUserLocation:
state.bindings.pinLocation = nil
case .shareLiveLocation:
completion?(.shareLiveLocation(timeout: Constants.liveLocationSharingDefaultTimeout))
}
@@ -35,7 +35,7 @@ class LocationSharingViewModelTests: XCTestCase {
XCTAssertNotNil(viewModel.context.viewState.mapStyleURL)
XCTAssertNotNil(viewModel.context.viewState.userAvatarData)
XCTAssertNil(viewModel.context.viewState.userAnnotation)
XCTAssertNil(viewModel.context.viewState.sharedAnnotation)
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
XCTAssertNil(viewModel.context.viewState.bindings.alertInfo)
}
@@ -63,7 +63,7 @@ class LocationSharingViewModelTests: XCTestCase {
let viewModel = buildViewModel(withLocation: false)
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
XCTAssertNil(viewModel.context.viewState.userAnnotation)
XCTAssertNil(viewModel.context.viewState.sharedAnnotation)
viewModel.context.send(viewAction: .share)
@@ -78,9 +78,9 @@ class LocationSharingViewModelTests: XCTestCase {
viewModel.completion = { result in
switch result {
case .share(let latitude, let longitude):
XCTAssertEqual(latitude, viewModel.context.viewState.userAnnotation?.coordinate.latitude)
XCTAssertEqual(longitude, viewModel.context.viewState.userAnnotation?.coordinate.longitude)
case .share(let latitude, let longitude, _):
XCTAssertEqual(latitude, viewModel.context.viewState.sharedAnnotation?.coordinate.latitude)
XCTAssertEqual(longitude, viewModel.context.viewState.sharedAnnotation?.coordinate.longitude)
expectation.fulfill()
case .cancel:
XCTFail()
@@ -88,7 +88,7 @@ class LocationSharingViewModelTests: XCTestCase {
}
XCTAssertNil(viewModel.context.viewState.bindings.userLocation)
XCTAssertNotNil(viewModel.context.viewState.userAnnotation)
XCTAssertNotNil(viewModel.context.viewState.sharedAnnotation)
viewModel.context.send(viewAction: .share)
@@ -123,6 +123,6 @@ class LocationSharingViewModelTests: XCTestCase {
private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel {
LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""),
location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil))
location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil), coordinateType: .user)
}
}
@@ -33,10 +33,10 @@ struct LocationSharingMapView: UIViewRepresentable {
let tileServerMapURL: URL
/// Map annotations
let annotations: [UserLocationAnnotation]
let annotations: [LocationAnnotation]
/// Map annotation to focus on
let highlightedAnnotation: UserLocationAnnotation?
let highlightedAnnotation: LocationAnnotation?
/// Current user avatar data, used to replace current location annotation view with the user avatar
let userAvatarData: AvatarInputProtocol?
@@ -47,6 +47,9 @@ struct LocationSharingMapView: UIViewRepresentable {
/// Last user location if `showsUserLocation` has been enabled
@Binding var userLocation: CLLocationCoordinate2D?
/// Coordinate of the center of the map
@Binding var mapCenterCoordinate: CLLocationCoordinate2D?
/// Publish view errors if any
let errorSubject: PassthroughSubject<LocationSharingViewError, Never>
@@ -68,7 +71,7 @@ struct LocationSharingMapView: UIViewRepresentable {
mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false)
}
if self.showsUserLocation {
if self.showsUserLocation && mapCenterCoordinate == nil {
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
} else {
@@ -114,10 +117,12 @@ extension LocationSharingMapView {
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if let userLocationAnnotation = annotation as? UserLocationAnnotation {
return UserLocationAnnotatonView(userLocationAnnotation: userLocationAnnotation)
} else if annotation is MGLUserLocation, let currentUserAvatarData = locationSharingMapView.userAvatarData {
// Replace default current location annotation view with a UserLocationAnnotatonView
return UserLocationAnnotatonView(avatarData: currentUserAvatarData)
return LocationAnnotatonView(userLocationAnnotation: userLocationAnnotation)
} else if let pinLocationAnnotation = annotation as? PinLocationAnnotation {
return LocationAnnotatonView(pinLocationAnnotation: pinLocationAnnotation)
} else if annotation is MGLUserLocation && locationSharingMapView.mapCenterCoordinate == nil, let currentUserAvatarData = locationSharingMapView.userAvatarData {
// Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location
return LocationAnnotatonView(avatarData: currentUserAvatarData)
}
return nil
@@ -145,6 +150,16 @@ extension LocationSharingMapView {
break
}
}
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
let mapCenterCoordinate = mapView.centerCoordinate
// Prevent this function to set pinLocation when the map is openning
guard let userLocation = locationSharingMapView.userLocation,
!userLocation.isEqual(to: mapCenterCoordinate, precision: 0.0000000001) else {
return
}
locationSharingMapView.mapCenterCoordinate = mapCenterCoordinate
}
}
}
@@ -40,6 +40,7 @@ struct LocationSharingMarkerView<Content: View>: View {
markerImage
.frame(width: 40, height: 40)
}
.offset(x: 0, y: -23)
}
}
@@ -33,13 +33,7 @@ struct LocationSharingView: View {
var body: some View {
NavigationView {
ZStack(alignment: .bottom) {
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
annotations: context.viewState.annotations,
highlightedAnnotation: context.viewState.highlightedAnnotation,
userAvatarData: context.viewState.userAvatarData,
showsUserLocation: context.viewState.showsUserLocation,
userLocation: $context.userLocation,
errorSubject: context.viewState.errorSubject)
mapView
VStack(spacing: 0) {
MapCreditsView()
if context.viewState.shareButtonVisible {
@@ -85,6 +79,39 @@ struct LocationSharingView: View {
.navigationViewStyle(StackNavigationViewStyle())
}
var mapView: some View {
ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) {
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
annotations: context.viewState.annotations,
highlightedAnnotation: context.viewState.highlightedAnnotation,
userAvatarData: context.viewState.userAvatarData,
showsUserLocation: context.viewState.showsUserLocation,
userLocation: $context.userLocation,
mapCenterCoordinate: $context.pinLocation,
errorSubject: context.viewState.errorSubject)
if context.viewState.isPinDropSharing {
LocationSharingMarkerView(backgroundColor: theme.colors.accent) {
Image(uiImage: Asset.Images.locationPinIcon.image)
.resizable()
.shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle())
}
}
}
Button {
context.send(viewAction: .goToUserLocation)
} label: {
Image(uiImage: Asset.Images.locationCenterMapIcon.image)
.foregroundColor(theme.colors.accent)
}
.padding(6.0)
.background(theme.colors.background)
.clipShape(RoundedCornerShape(radius: 4, corners: [.allCorners]))
.shadow(radius: 2.0)
.offset(x: -11.0, y: 52)
}
}
var buttonsView: some View {
VStack(alignment: .leading, spacing: 15) {
if !context.viewState.isPinDropSharing {
@@ -107,7 +134,7 @@ struct LocationSharingView: View {
}
} else {
LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) {
// TODO: - Pin drop sharing action
context.send(viewAction: .sharePinLocation)
} buttonIcon: {
Image(uiImage: Asset.Images.locationPinIcon.image)
.resizable()
@@ -19,7 +19,7 @@ import SwiftUI
import Mapbox
@available(iOS 14, *)
class UserLocationAnnotatonView: MGLUserLocationAnnotationView {
class LocationAnnotatonView: MGLUserLocationAnnotationView {
// MARK: Private
@@ -39,6 +39,14 @@ class UserLocationAnnotatonView: MGLUserLocationAnnotationView {
super.init(annotation: userLocationAnnotation, reuseIdentifier: nil)
self.addUserMarkerView(with: userLocationAnnotation.avatarData)
}
init(pinLocationAnnotation: PinLocationAnnotation) {
// TODO: Use a reuseIdentifier
super.init(annotation: pinLocationAnnotation, reuseIdentifier: nil)
self.addPinMarkerView()
}
required init?(coder: NSCoder) {
@@ -55,12 +63,26 @@ class UserLocationAnnotatonView: MGLUserLocationAnnotationView {
}).view else {
return
}
addMarkerView(with: avatarImageView)
}
private func addPinMarkerView() {
guard let pinImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) {
Image(uiImage: Asset.Images.locationPinIcon.image)
.resizable()
.shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle())
}).view else {
return
}
addMarkerView(with: pinImageView)
}
private func addMarkerView(with imageView: UIView) {
addSubview(imageView)
addSubview(avatarImageView)
addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor),
leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)])
addConstraints([topAnchor.constraint(equalTo: imageView.topAnchor),
leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
trailingAnchor.constraint(equalTo: imageView.trailingAnchor)])
}
}
@@ -251,6 +251,25 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy
}
}
extension MXRoom {
public var isMuted: Bool {
// Check whether an override rule has been defined with the roomm id as rule id.
// This kind of rule is created to mute the room
guard let rule = self.overridePushRule,
rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify),
rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else {
return false
}
return rule.enabled
}
public var isMentionsOnly: Bool {
// Check push rules at room level
guard let rule = roomPushRule else { return false }
return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify)
}
}
// We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment
fileprivate extension MXRoom {
@@ -288,23 +307,6 @@ fileprivate extension MXRoom {
return .all
}
var isMuted: Bool {
// Check whether an override rule has been defined with the roomm id as rule id.
// This kind of rule is created to mute the room
guard let rule = self.overridePushRule,
rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify),
rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else {
return false
}
return rule.enabled
}
var isMentionsOnly: Bool {
// Check push rules at room level
guard let rule = roomPushRule else { return false }
return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify)
}
}
fileprivate extension MXPushRule {
@@ -109,7 +109,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol {
switch self.selectedType {
case .private:
_joinRule = .private
_joinRule = .invite
case .public:
_joinRule = .public
case .restricted: