mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-03 06:36:58 +02:00
Show own location in map views (#7375)
* show own location on static sharing * show own location on live sharing * add tests and changelog * check location authorisation, fix center to current user location button * it moves request to LocationManager in proper service * add static location viewer service
This commit is contained in:
+7
@@ -42,6 +42,12 @@ struct LiveLocationSharingViewerViewState: BindableState {
|
||||
/// Live location list items
|
||||
var listItemsViewData: [LiveLocationListItemViewData]
|
||||
|
||||
var showsUserLocation = false
|
||||
|
||||
var isCurrentUserShared: Bool {
|
||||
listItemsViewData.contains { $0.isCurrentUser }
|
||||
}
|
||||
|
||||
var showLoadingIndicator = false
|
||||
|
||||
var shareButtonEnabled: Bool {
|
||||
@@ -75,4 +81,5 @@ enum LiveLocationSharingViewerViewAction {
|
||||
case tapListItem(_ userId: String)
|
||||
case share(_ annotation: UserLocationAnnotation)
|
||||
case mapCreditsDidTap
|
||||
case showUserLocation
|
||||
}
|
||||
|
||||
+10
@@ -72,6 +72,8 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType
|
||||
completion?(.share(userLocationAnnotation.coordinate))
|
||||
case .mapCreditsDidTap:
|
||||
state.bindings.showMapCreditsSheet.toggle()
|
||||
case .showUserLocation:
|
||||
showsCurrentUserLocation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,4 +231,12 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showsCurrentUserLocation() {
|
||||
if liveLocationSharingViewerService.requestAuthorizationIfNeeded() {
|
||||
state.showsUserLocation = true
|
||||
} else {
|
||||
state.errorSubject.send(.invalidLocationAuthorization)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -33,4 +33,6 @@ protocol LiveLocationSharingViewerServiceProtocol {
|
||||
|
||||
/// Stop current user location sharing
|
||||
func stopUserLiveLocationSharing(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool
|
||||
}
|
||||
|
||||
+6
@@ -19,11 +19,13 @@ import Foundation
|
||||
import MatrixSDK
|
||||
|
||||
class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private(set) var usersLiveLocation: [UserLiveLocation] = []
|
||||
private let roomId: String
|
||||
private var beaconInfoSummaryListener: Any?
|
||||
private let locationManager = CLLocationManager()
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@@ -74,6 +76,10 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool {
|
||||
locationManager.requestAuthorizationIfNeeded()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateUsersLiveLocation(notifyUpdate: Bool) {
|
||||
|
||||
+12
-3
@@ -27,12 +27,17 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
init(generateRandomUsers: Bool = false) {
|
||||
let firstUserLiveLocation = createFirstUserLiveLocation()
|
||||
init(generateRandomUsers: Bool = false, currentUserSharingLocation: Bool = true) {
|
||||
let firstUserLiveLocation: UserLiveLocation?
|
||||
if currentUserSharingLocation {
|
||||
firstUserLiveLocation = createFirstUserLiveLocation()
|
||||
} else {
|
||||
firstUserLiveLocation = nil
|
||||
}
|
||||
|
||||
let secondUserLiveLocation = createSecondUserLiveLocation()
|
||||
|
||||
var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation]
|
||||
var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation].compactMap { $0 }
|
||||
|
||||
if generateRandomUsers {
|
||||
for _ in 1...20 {
|
||||
@@ -56,6 +61,10 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt
|
||||
|
||||
func stopUserLiveLocationSharing(completion: @escaping (Result<Void, Error>) -> Void) { }
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func createFirstUserLiveLocation() -> UserLiveLocation {
|
||||
|
||||
+14
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreLocation
|
||||
import XCTest
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
@@ -30,4 +31,17 @@ class LiveLocationSharingViewerViewModelTests: XCTestCase {
|
||||
viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testIsUserBeingShared() {
|
||||
XCTAssertTrue(context.viewState.isCurrentUserShared)
|
||||
}
|
||||
|
||||
func testToggleShowUserLocation() {
|
||||
let service = MockLiveLocationSharingViewerService(currentUserSharingLocation: false)
|
||||
let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service)
|
||||
XCTAssertFalse(viewModel.context.viewState.isCurrentUserShared)
|
||||
XCTAssertFalse(viewModel.context.viewState.showsUserLocation)
|
||||
viewModel.context.send(viewAction: .showUserLocation)
|
||||
XCTAssertTrue(viewModel.context.viewState.showsUserLocation)
|
||||
}
|
||||
}
|
||||
|
||||
+50
-14
@@ -34,23 +34,35 @@ struct LiveLocationSharingViewer: View {
|
||||
|
||||
@ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context
|
||||
|
||||
var mapView: LocationSharingMapView {
|
||||
LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL,
|
||||
annotations: viewModel.viewState.annotations,
|
||||
highlightedAnnotation: viewModel.viewState.highlightedAnnotation,
|
||||
userAvatarData: nil,
|
||||
showsUserLocation: viewModel.viewState.showsUserLocation,
|
||||
userAnnotationCanShowCallout: true,
|
||||
userLocation: Binding.constant(nil),
|
||||
mapCenterCoordinate: Binding.constant(nil),
|
||||
onCalloutTap: { annotation in
|
||||
if let userLocationAnnotation = annotation as? UserLocationAnnotation {
|
||||
viewModel.send(viewAction: .share(userLocationAnnotation))
|
||||
}
|
||||
},
|
||||
errorSubject: viewModel.viewState.errorSubject)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
if !viewModel.viewState.showMapLoadingError {
|
||||
LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL,
|
||||
annotations: viewModel.viewState.annotations,
|
||||
highlightedAnnotation: viewModel.viewState.highlightedAnnotation,
|
||||
userAvatarData: nil,
|
||||
showsUserLocation: false,
|
||||
userAnnotationCanShowCallout: true,
|
||||
userLocation: Binding.constant(nil),
|
||||
mapCenterCoordinate: Binding.constant(nil),
|
||||
onCalloutTap: { annotation in
|
||||
if let userLocationAnnotation = annotation as? UserLocationAnnotation {
|
||||
viewModel.send(viewAction: .share(userLocationAnnotation))
|
||||
}
|
||||
},
|
||||
errorSubject: viewModel.viewState.errorSubject)
|
||||
|
||||
if !viewModel.viewState.isCurrentUserShared {
|
||||
mapView
|
||||
.overlay(CenterToUserLocationButton(action: {
|
||||
viewModel.send(viewAction: .showUserLocation)
|
||||
}).offset(x: -11.0, y: 52), alignment: .topTrailing)
|
||||
} else {
|
||||
mapView
|
||||
}
|
||||
|
||||
// Show map credits above collapsed bottom sheet height if bottom sheet is visible
|
||||
if viewModel.viewState.isBottomSheetVisible {
|
||||
@@ -178,3 +190,27 @@ struct LiveLocationSharingViewer_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CenterToUserLocationButton: View {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationCenterMapIcon.image)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
}
|
||||
.padding(8.0)
|
||||
.background(theme.colors.background)
|
||||
.clipShape(Circle())
|
||||
.shadow(radius: 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ struct MapViewErrorAlertInfoBuilder {
|
||||
case .invalidLocationAuthorization:
|
||||
alertInfo = AlertInfo(id: .authorizationError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, {}),
|
||||
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion))
|
||||
default:
|
||||
alertInfo = nil
|
||||
|
||||
@@ -75,7 +75,7 @@ struct LocationSharingMapView: UIViewRepresentable {
|
||||
mapView.vc_removeAllAnnotations()
|
||||
mapView.addAnnotations(annotations)
|
||||
|
||||
if let highlightedAnnotation = highlightedAnnotation {
|
||||
if let highlightedAnnotation = highlightedAnnotation, !showsUserLocation {
|
||||
mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false)
|
||||
}
|
||||
|
||||
@@ -125,11 +125,14 @@ extension LocationSharingMapView {
|
||||
return LocationAnnotationView(userLocationAnnotation: userLocationAnnotation)
|
||||
} else if let pinLocationAnnotation = annotation as? PinLocationAnnotation {
|
||||
return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation)
|
||||
} else if annotation is MGLUserLocation, let currentUserAvatarData = locationSharingMapView.userAvatarData {
|
||||
// Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location
|
||||
return LocationAnnotationView(avatarData: currentUserAvatarData)
|
||||
} else if annotation is MGLUserLocation {
|
||||
if let currentUserAvatarData = locationSharingMapView.userAvatarData {
|
||||
// Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location
|
||||
return LocationAnnotationView(avatarData: currentUserAvatarData)
|
||||
} else {
|
||||
return LocationAnnotationView(userPinLocationAnnotation: annotation)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ class LocationAnnotationView: MGLUserLocationAnnotationView {
|
||||
|
||||
addUserMarkerView(with: userLocationAnnotation.avatarData)
|
||||
}
|
||||
|
||||
convenience init(userPinLocationAnnotation: MGLAnnotation) {
|
||||
self.init(annotation: userPinLocationAnnotation, reuseIdentifier: "userPinLocation")
|
||||
|
||||
addPinView()
|
||||
}
|
||||
convenience init(pinLocationAnnotation: PinLocationAnnotation) {
|
||||
// TODO: Use a reuseIdentifier
|
||||
self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil)
|
||||
@@ -74,6 +78,16 @@ class LocationAnnotationView: MGLUserLocationAnnotationView {
|
||||
addMarkerView(avatarMarkerView)
|
||||
}
|
||||
|
||||
private func addPinView() {
|
||||
guard let pinView = UIHostingController(rootView: Image(uiImage: Asset.Images.locationMarkerIcon.image)
|
||||
.resizable()
|
||||
.foregroundColor(theme.colors.accent)).view else {
|
||||
return
|
||||
}
|
||||
|
||||
addMarkerView(pinView)
|
||||
}
|
||||
|
||||
private func addPinMarkerView() {
|
||||
guard let pinMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) {
|
||||
Image(uiImage: Asset.Images.locationPinIcon.image)
|
||||
|
||||
+2
-1
@@ -53,7 +53,8 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable {
|
||||
mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL,
|
||||
avatarData: parameters.avatarData,
|
||||
location: parameters.location,
|
||||
coordinateType: parameters.coordinateType
|
||||
coordinateType: parameters.coordinateType,
|
||||
service: StaticLocationSharingViewerService()
|
||||
)
|
||||
let view = StaticLocationView(viewModel: viewModel.context)
|
||||
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
|
||||
|
||||
+2
-1
@@ -46,7 +46,8 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable {
|
||||
let viewModel = StaticLocationViewingViewModel(mapStyleURL: mapStyleURL,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
|
||||
location: location,
|
||||
coordinateType: coordinateType)
|
||||
coordinateType: coordinateType,
|
||||
service: MockStaticLocationSharingViewerService())
|
||||
|
||||
return ([viewModel],
|
||||
AnyView(StaticLocationView(viewModel: viewModel.context)
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
class StaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let locationManager = CLLocationManager()
|
||||
|
||||
// MARK: Public
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool {
|
||||
locationManager.requestAuthorizationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MockStaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol {
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol StaticLocationSharingViewerServiceProtocol {
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool
|
||||
}
|
||||
+3
@@ -23,6 +23,7 @@ import Foundation
|
||||
enum StaticLocationViewingViewAction {
|
||||
case close
|
||||
case share
|
||||
case showUserLocation
|
||||
}
|
||||
|
||||
enum StaticLocationViewingViewModelResult {
|
||||
@@ -42,6 +43,8 @@ struct StaticLocationViewingViewState: BindableState {
|
||||
/// Shared annotation to display existing location
|
||||
let sharedAnnotation: LocationAnnotation
|
||||
|
||||
var showsUserLocation = false
|
||||
|
||||
var showLoadingIndicator = false
|
||||
|
||||
var shareButtonEnabled: Bool {
|
||||
|
||||
+15
-1
@@ -24,6 +24,7 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var staticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol
|
||||
private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder
|
||||
|
||||
// MARK: Public
|
||||
@@ -32,7 +33,10 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType) {
|
||||
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType, service: StaticLocationSharingViewerServiceProtocol) {
|
||||
|
||||
staticLocationSharingViewerService = service
|
||||
|
||||
let sharedAnnotation: LocationAnnotation
|
||||
switch coordinateType {
|
||||
case .user:
|
||||
@@ -63,6 +67,8 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static
|
||||
completion?(.close)
|
||||
case .share:
|
||||
completion?(.share(state.sharedAnnotation.coordinate))
|
||||
case .showUserLocation:
|
||||
showsCurrentUserLocation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +95,12 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static
|
||||
|
||||
state.bindings.alertInfo = alertInfo
|
||||
}
|
||||
|
||||
private func showsCurrentUserLocation() {
|
||||
if staticLocationSharingViewerService.requestAuthorizationIfNeeded() {
|
||||
state.showsUserLocation = true
|
||||
} else {
|
||||
state.errorSubject.send(.invalidLocationAuthorization)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -79,10 +79,18 @@ class StaticLocationViewingViewModelTests: XCTestCase {
|
||||
waitForExpectations(timeout: 3)
|
||||
}
|
||||
|
||||
func testToggleShowUserLocation() {
|
||||
let viewModel = buildViewModel()
|
||||
XCTAssertFalse(viewModel.context.viewState.showsUserLocation)
|
||||
viewModel.context.send(viewAction: .showUserLocation)
|
||||
XCTAssertTrue(viewModel.context.viewState.showsUserLocation)
|
||||
}
|
||||
|
||||
private func buildViewModel() -> StaticLocationViewingViewModel {
|
||||
StaticLocationViewingViewModel(mapStyleURL: URL(string: "http://empty.com")!,
|
||||
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""),
|
||||
location: CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096),
|
||||
coordinateType: .user)
|
||||
coordinateType: .user,
|
||||
service: MockStaticLocationSharingViewerService())
|
||||
}
|
||||
}
|
||||
|
||||
+15
-8
@@ -29,19 +29,26 @@ struct StaticLocationView: View {
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var mapView: LocationSharingMapView {
|
||||
LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL,
|
||||
annotations: [viewModel.viewState.sharedAnnotation],
|
||||
highlightedAnnotation: viewModel.viewState.sharedAnnotation,
|
||||
userAvatarData: nil,
|
||||
showsUserLocation: viewModel.viewState.showsUserLocation,
|
||||
userLocation: Binding.constant(nil),
|
||||
mapCenterCoordinate: Binding.constant(nil),
|
||||
errorSubject: viewModel.viewState.errorSubject)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottom) {
|
||||
LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL,
|
||||
annotations: [viewModel.viewState.sharedAnnotation],
|
||||
highlightedAnnotation: viewModel.viewState.sharedAnnotation,
|
||||
userAvatarData: viewModel.viewState.userAvatarData,
|
||||
showsUserLocation: false,
|
||||
userLocation: Binding.constant(nil),
|
||||
mapCenterCoordinate: Binding.constant(nil),
|
||||
errorSubject: viewModel.viewState.errorSubject)
|
||||
mapView
|
||||
MapCreditsView()
|
||||
}
|
||||
.overlay(CenterToUserLocationButton(action: {
|
||||
viewModel.send(viewAction: .showUserLocation)
|
||||
}).offset(x: -11.0, y: 52), alignment: .topTrailing)
|
||||
.ignoresSafeArea(.all, edges: [.bottom])
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
||||
Reference in New Issue
Block a user