Merge branch 'develop' into bugfix/PRODDEF-1289_accessibilityChanges
This commit is contained in:
commit
9d4c89d37e
@ -26,6 +26,7 @@
|
|||||||
public var localBundle: Bundle?
|
public var localBundle: Bundle?
|
||||||
public var cornerRadius: CGFloat?
|
public var cornerRadius: CGFloat?
|
||||||
public var clipsImage: Bool?
|
public var clipsImage: Bool?
|
||||||
|
public var allowServerParameters: Bool?
|
||||||
public var shouldMaskRecordedView: Bool? = false
|
public var shouldMaskRecordedView: Bool? = false
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -56,5 +57,6 @@
|
|||||||
case cornerRadius
|
case cornerRadius
|
||||||
case clipsImage
|
case clipsImage
|
||||||
case shouldMaskRecordedView
|
case shouldMaskRecordedView
|
||||||
|
case allowServerParameters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -289,7 +289,7 @@
|
|||||||
if shouldLoadImage(withName: imageModel.image, width: width, height: height) {
|
if shouldLoadImage(withName: imageModel.image, width: width, height: height) {
|
||||||
imageView.image = nil
|
imageView.image = nil
|
||||||
imageView.animatedImage = nil
|
imageView.animatedImage = nil
|
||||||
loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width, height: height, customFallbackImage: imageModel.fallbackImage, localBundle: imageModel.localBundle)
|
loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width, height: height, customFallbackImage: imageModel.fallbackImage, allowServerParameters: imageModel.allowServerParameters ?? false, localBundle: imageModel.localBundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let contentMode = imageModel.contentMode {
|
if let contentMode = imageModel.contentMode {
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import VDSColorTokens
|
|||||||
public let selectionLineMovingTime: TimeInterval = 0.2
|
public let selectionLineMovingTime: TimeInterval = 0.2
|
||||||
|
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// MARK:- Layout Views
|
// MARK: - Layout Views
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
|
|
||||||
open override func reset() {
|
open override func reset() {
|
||||||
@ -122,8 +122,27 @@ import VDSColorTokens
|
|||||||
NSLayoutConstraint.constraintPinSubview(bottomLine, pinTop: false, pinBottom: true, pinLeft: true, pinRight: true)
|
NSLayoutConstraint.constraintPinSubview(bottomLine, pinTop: false, pinBottom: true, pinLeft: true, pinRight: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
// Accounts for any collection size changes
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.layoutCollection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invalidates the layout and ensures we are paged to the correct cell.
|
||||||
|
open func layoutCollection() {
|
||||||
|
collectionView?.collectionViewLayout.invalidateLayout()
|
||||||
|
|
||||||
|
// Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.collectionView?.scrollToItem(at: IndexPath(row: self.selectedIndex, section: 0), at: .left, animated: false)
|
||||||
|
self.collectionView?.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// MARK:- Control Methods
|
// MARK: - Control Methods
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
|
|
||||||
public func selectIndex(_ index: Int, animated: Bool) {
|
public func selectIndex(_ index: Int, animated: Bool) {
|
||||||
@ -146,7 +165,7 @@ import VDSColorTokens
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// MARK:- Molecule Setup
|
// MARK: - Molecule Setup
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
|
|
||||||
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
||||||
@ -163,7 +182,7 @@ import VDSColorTokens
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// MARK:- Collection View Methods
|
// MARK: - Collection View Methods
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
|
|
||||||
extension Tabs: UICollectionViewDataSource {
|
extension Tabs: UICollectionViewDataSource {
|
||||||
@ -283,7 +302,7 @@ extension Tabs: UIScrollViewDelegate {
|
|||||||
|
|
||||||
|
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
// MARK:- Bottom Line Methods
|
// MARK: - Bottom Line Methods
|
||||||
//-------------------------------------------------
|
//-------------------------------------------------
|
||||||
extension Tabs {
|
extension Tabs {
|
||||||
func moveSelectionLine(toIndex indexPath: IndexPath, animated: Bool, cell: TabItemCell) {
|
func moveSelectionLine(toIndex indexPath: IndexPath, animated: Bool, cell: TabItemCell) {
|
||||||
|
|||||||
@ -22,7 +22,8 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol {
|
|||||||
|
|
||||||
open override func setupView() {
|
open override func setupView() {
|
||||||
super.setupView()
|
super.setupView()
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
// Covers the card when peaking.
|
// Covers the card when peaking.
|
||||||
peakingCover.backgroundColor = .white
|
peakingCover.backgroundColor = .white
|
||||||
peakingCover.alpha = 0
|
peakingCover.alpha = 0
|
||||||
@ -51,6 +52,12 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol {
|
|||||||
super.set(with: model, delegateObject, additionalData)
|
super.set(with: model, delegateObject, additionalData)
|
||||||
guard let collectionModel = model as? CarouselItemModel else { return }
|
guard let collectionModel = model as? CarouselItemModel else { return }
|
||||||
|
|
||||||
|
if let cornerRadius = (model as? ContainerModel)?.cornerRadius {
|
||||||
|
layer.cornerRadius = cornerRadius
|
||||||
|
} else {
|
||||||
|
layer.cornerRadius = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Handles peaking.
|
// Handles peaking.
|
||||||
allowsPeaking = collectionModel.peakingUI ?? false
|
allowsPeaking = collectionModel.peakingUI ?? false
|
||||||
if let peakingArrowColor = collectionModel.peakingArrowColor {
|
if let peakingArrowColor = collectionModel.peakingArrowColor {
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
open override class var identifier: String { "collectionItem" }
|
open override class var identifier: String { "collectionItem" }
|
||||||
|
|
||||||
public var action: ActionModelProtocol?
|
public var action: ActionModelProtocol?
|
||||||
|
public var border = false
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Keys
|
// MARK: - Keys
|
||||||
@ -23,6 +24,7 @@
|
|||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case action
|
case action
|
||||||
|
case border
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -59,12 +61,16 @@
|
|||||||
required public init(from decoder: Decoder) throws {
|
required public init(from decoder: Decoder) throws {
|
||||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||||
|
if let border = try typeContainer.decodeIfPresent(Bool.self, forKey: .border) {
|
||||||
|
self.border = border
|
||||||
|
}
|
||||||
try super.init(from: decoder)
|
try super.init(from: decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func encode(to encoder: Encoder) throws {
|
public override func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encodeModelIfPresent(action, forKey: .action)
|
try container.encodeModelIfPresent(action, forKey: .action)
|
||||||
|
try container.encodeIfPresent(border, forKey: .border)
|
||||||
try super.encode(to: encoder)
|
try super.encode(to: encoder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,13 @@ open class MoleculeCollectionViewCell: CollectionViewCell {
|
|||||||
super.set(with: model, delegateObject, additionalData)
|
super.set(with: model, delegateObject, additionalData)
|
||||||
guard let collectionModel = model as? MoleculeCollectionItemModel else { return }
|
guard let collectionModel = model as? MoleculeCollectionItemModel else { return }
|
||||||
|
|
||||||
|
if collectionModel.border {
|
||||||
|
contentView.layer.borderColor = UIColor.black.cgColor
|
||||||
|
contentView.layer.borderWidth = 1
|
||||||
|
} else {
|
||||||
|
contentView.layer.borderWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
if molecule == nil {
|
if molecule == nil {
|
||||||
if let moleculeView = ModelRegistry.createMolecule(collectionModel.molecule, delegateObject: delegateObject, additionalData: additionalData) {
|
if let moleculeView = ModelRegistry.createMolecule(collectionModel.molecule, delegateObject: delegateObject, additionalData: additionalData) {
|
||||||
addMolecule(moleculeView)
|
addMolecule(moleculeView)
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import UIKit
|
|||||||
public override func reset() {
|
public override func reset() {
|
||||||
super.reset()
|
super.reset()
|
||||||
tabs.reset()
|
tabs.reset()
|
||||||
|
tabs.paddingBeforeFirstTab = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 46 }
|
public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 46 }
|
||||||
|
|||||||
@ -104,6 +104,7 @@ open class Carousel: View {
|
|||||||
super.setupView()
|
super.setupView()
|
||||||
collectionView.dataSource = self
|
collectionView.dataSource = self
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
|
collectionView.bounces = false
|
||||||
addSubview(collectionView)
|
addSubview(collectionView)
|
||||||
bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
|
bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
|
||||||
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
|
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
|
||||||
@ -129,7 +130,7 @@ open class Carousel: View {
|
|||||||
inset.left = carouselModel?.leftPadding ?? Padding.Component.horizontalPaddingForSize(size)
|
inset.left = carouselModel?.leftPadding ?? Padding.Component.horizontalPaddingForSize(size)
|
||||||
inset.right = carouselModel?.rightPadding ?? Padding.Component.horizontalPaddingForSize(size)
|
inset.right = carouselModel?.rightPadding ?? Padding.Component.horizontalPaddingForSize(size)
|
||||||
}
|
}
|
||||||
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset = inset
|
collectionView.contentInset = inset
|
||||||
|
|
||||||
// Update cells and re-layout.
|
// Update cells and re-layout.
|
||||||
for cell in collectionView.visibleCells {
|
for cell in collectionView.visibleCells {
|
||||||
|
|||||||
@ -100,7 +100,8 @@
|
|||||||
let columns = templateModel?.columns, columns > 0,
|
let columns = templateModel?.columns, columns > 0,
|
||||||
let cell = cell as? CollectionTemplateItemProtocol {
|
let cell = cell as? CollectionTemplateItemProtocol {
|
||||||
let width = (size - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right) / CGFloat(columns)
|
let width = (size - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right) / CGFloat(columns)
|
||||||
cell.set(width: width)
|
// Makes the width slightly less to avoid rounding errors and the column being a little too big.
|
||||||
|
cell.set(width: width - 0.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,11 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
|
|||||||
override open func viewForBottom() -> UIView {
|
override open func viewForBottom() -> UIView {
|
||||||
guard let footerModel = templateModel?.footer,
|
guard let footerModel = templateModel?.footer,
|
||||||
let molecule = generateMoleculeView(from: footerModel)
|
let molecule = generateMoleculeView(from: footerModel)
|
||||||
else { return super.viewForBottom() }
|
else {
|
||||||
|
let view = super.viewForBottom()
|
||||||
|
view.backgroundColor = templateModel?.backgroundColor?.uiColor ?? .clear
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
return molecule
|
return molecule
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,6 +169,7 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController {
|
|||||||
bottomView = MVMCoreUICommonViewsUtility.getView(with: 0.5)
|
bottomView = MVMCoreUICommonViewsUtility.getView(with: 0.5)
|
||||||
}
|
}
|
||||||
let footerView = MVMCoreUICommonViewsUtility.commonView()
|
let footerView = MVMCoreUICommonViewsUtility.commonView()
|
||||||
|
footerView.backgroundColor = bottomView.backgroundColor
|
||||||
footerView.addSubview(bottomView)
|
footerView.addSubview(bottomView)
|
||||||
bottomViewTopConstraint = bottomView.topAnchor.constraint(equalTo: footerView.topAnchor, constant: spaceAboveBottomView() ?? 0)
|
bottomViewTopConstraint = bottomView.topAnchor.constraint(equalTo: footerView.topAnchor, constant: spaceAboveBottomView() ?? 0)
|
||||||
bottomViewTopConstraint?.isActive = true
|
bottomViewTopConstraint?.isActive = true
|
||||||
|
|||||||
@ -435,7 +435,7 @@ import MVMCore
|
|||||||
|
|
||||||
// Open the support panel
|
// Open the support panel
|
||||||
if error == nil,
|
if error == nil,
|
||||||
loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true {
|
(loadObject?.requestParameters?.openSupportPanel ?? false) || (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) {
|
||||||
MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true)
|
MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,7 +100,13 @@ public extension UINavigationController {
|
|||||||
appearance.backgroundColor = backgroundColor
|
appearance.backgroundColor = backgroundColor
|
||||||
appearance.titleTextAttributes.updateValue(tint, forKey: .foregroundColor)
|
appearance.titleTextAttributes.updateValue(tint, forKey: .foregroundColor)
|
||||||
appearance.titlePositionAdjustment = model.titleOffset ?? .zero
|
appearance.titlePositionAdjustment = model.titleOffset ?? .zero
|
||||||
appearance.shadowColor = model.line?.backgroundColor?.uiColor ?? .clear
|
if let type = model.line?.type,
|
||||||
|
type != .none,
|
||||||
|
let color = model.line?.backgroundColor {
|
||||||
|
appearance.shadowColor = color.uiColor
|
||||||
|
} else {
|
||||||
|
appearance.shadowColor = .clear
|
||||||
|
}
|
||||||
appearance.shadowImage = getNavigationBarShadowImage(for: model)?.withRenderingMode(.alwaysTemplate)
|
appearance.shadowImage = getNavigationBarShadowImage(for: model)?.withRenderingMode(.alwaysTemplate)
|
||||||
navigationBar.standardAppearance = appearance
|
navigationBar.standardAppearance = appearance
|
||||||
navigationBar.scrollEdgeAppearance = appearance
|
navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
|||||||
@ -31,6 +31,11 @@ open class Container: View, ContainerProtocol {
|
|||||||
guard let containerModel = model as? ContainerModelProtocol else { return }
|
guard let containerModel = model as? ContainerModelProtocol else { return }
|
||||||
|
|
||||||
containerHelper.set(with: containerModel, for: view as? MVMCoreUIViewConstrainingProtocol)
|
containerHelper.set(with: containerModel, for: view as? MVMCoreUIViewConstrainingProtocol)
|
||||||
|
if let cornerRadius = (containerModel as? ContainerModel)?.cornerRadius {
|
||||||
|
layer.cornerRadius = cornerRadius
|
||||||
|
} else {
|
||||||
|
layer.cornerRadius = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override open func reset() {
|
override open func reset() {
|
||||||
|
|||||||
@ -23,6 +23,8 @@ open class ContainerModel: ContainerModelProtocol, Codable {
|
|||||||
public var topPadding: CGFloat?
|
public var topPadding: CGFloat?
|
||||||
public var bottomPadding: CGFloat?
|
public var bottomPadding: CGFloat?
|
||||||
|
|
||||||
|
public var cornerRadius: CGFloat?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Keys
|
// MARK: - Keys
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -36,6 +38,7 @@ open class ContainerModel: ContainerModelProtocol, Codable {
|
|||||||
case useVerticalMargins
|
case useVerticalMargins
|
||||||
case topPadding
|
case topPadding
|
||||||
case bottomPadding
|
case bottomPadding
|
||||||
|
case cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -83,6 +86,7 @@ open class ContainerModel: ContainerModelProtocol, Codable {
|
|||||||
useVerticalMargins = try typeContainer.decodeIfPresent(Bool.self, forKey: .useVerticalMargins)
|
useVerticalMargins = try typeContainer.decodeIfPresent(Bool.self, forKey: .useVerticalMargins)
|
||||||
topPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .topPadding)
|
topPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .topPadding)
|
||||||
bottomPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .bottomPadding)
|
bottomPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .bottomPadding)
|
||||||
|
cornerRadius = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .cornerRadius)
|
||||||
setDefaults()
|
setDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,5 +100,6 @@ open class ContainerModel: ContainerModelProtocol, Codable {
|
|||||||
try container.encodeIfPresent(useVerticalMargins, forKey: .useVerticalMargins)
|
try container.encodeIfPresent(useVerticalMargins, forKey: .useVerticalMargins)
|
||||||
try container.encodeIfPresent(topPadding, forKey: .topPadding)
|
try container.encodeIfPresent(topPadding, forKey: .topPadding)
|
||||||
try container.encodeIfPresent(bottomPadding, forKey: .bottomPadding)
|
try container.encodeIfPresent(bottomPadding, forKey: .bottomPadding)
|
||||||
|
try container.encodeIfPresent(cornerRadius, forKey: .cornerRadius)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public extension MVMCoreUIViewControllerMappingObject {
|
|||||||
add(toTemplateViewControllerMapping: ["modalStack": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeStackTemplate.self)!])
|
add(toTemplateViewControllerMapping: ["modalStack": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeStackTemplate.self)!])
|
||||||
|
|
||||||
register(template: MoleculeListTemplate.self)
|
register(template: MoleculeListTemplate.self)
|
||||||
|
register(template: CollectionTemplate.self)
|
||||||
add(toTemplateViewControllerMapping: ["modalList": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeListTemplate.self)!])
|
add(toTemplateViewControllerMapping: ["modalList": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeListTemplate.self)!])
|
||||||
add(toTemplateViewControllerMapping: [SectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: SectionListTemplate.self)!])
|
add(toTemplateViewControllerMapping: [SectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: SectionListTemplate.self)!])
|
||||||
add(toTemplateViewControllerMapping: [ModalSectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: ModalSectionListTemplate.self)!])
|
add(toTemplateViewControllerMapping: [ModalSectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: ModalSectionListTemplate.self)!])
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user