refactored popover controlling algo
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
7cd9a7d1a9
commit
8757fe6147
@ -26,22 +26,13 @@ open class DatePicker: EntryFieldBase {
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
/// A callback when the selected option changes. Passes parameters (option).
|
/// A callback when the selected option changes. Passes parameters (option).
|
||||||
open var onDateSelected: ((Date, DatePicker) -> Void)?
|
open var onDateSelected: ((Date, DatePicker) -> Void)?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Private Properties
|
// MARK: - Private Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
internal override var responder: UIResponder? { hiddenView }
|
internal override var responder: UIResponder? { hiddenView }
|
||||||
internal var hiddenView = UITextView().with { $0.width(0) }
|
internal var hiddenView = UITextView().with { $0.width(0) }
|
||||||
internal var minWidthDefault = 186.0
|
internal var minWidthDefault = 186.0
|
||||||
internal var popoverView: UIScrollView!
|
|
||||||
internal var popoverVisible = false
|
|
||||||
internal var outsideTapGesture: UITapGestureRecognizer?
|
|
||||||
internal var outsidePanGesture: UIPanGestureRecognizer?
|
|
||||||
internal var overlayView = UIView().with {
|
|
||||||
$0.backgroundColor = .clear;
|
|
||||||
$0.isHidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
internal var bottomStackView: UIStackView = {
|
internal var bottomStackView: UIStackView = {
|
||||||
return UIStackView().with {
|
return UIStackView().with {
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@ -51,6 +42,24 @@ open class DatePicker: EntryFieldBase {
|
|||||||
$0.spacing = VDSLayout.space2X
|
$0.spacing = VDSLayout.space2X
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Private Popover/Alert Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
/// View shown inline
|
||||||
|
internal var popoverView: UIView!
|
||||||
|
/// Size used for the popover
|
||||||
|
internal var popoverViewSize: CGSize = .zero
|
||||||
|
/// Spacing between the popover and the ContainerView when not a AlertViewController
|
||||||
|
internal var popoverSpacing: CGFloat = VDSLayout.space1X
|
||||||
|
/// Whether or not the popover is visible
|
||||||
|
internal var popoverVisible = false
|
||||||
|
/// If the ContainerView exists somewhere in the superview hierarch in a ScrollView.
|
||||||
|
internal var scrollView: UIScrollView?
|
||||||
|
/// Original Found ScrollView ContentSize, this will get reset back to this size when the Popover is removed.
|
||||||
|
internal var scrollViewContentSize: CGSize?
|
||||||
|
/// Presenting ViewController with showing the AlertViewController Version.
|
||||||
|
internal var topViewController: UIViewController?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Public Properties
|
// MARK: - Public Properties
|
||||||
@ -97,12 +106,12 @@ open class DatePicker: EntryFieldBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
|
open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } }
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Configuration Properties
|
// MARK: - Configuration Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
|
internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) }
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Overrides
|
// MARK: - Overrides
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -110,10 +119,10 @@ open class DatePicker: EntryFieldBase {
|
|||||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
||||||
open override func setup() {
|
open override func setup() {
|
||||||
super.setup()
|
super.setup()
|
||||||
|
|
||||||
// setting color config
|
// setting color config
|
||||||
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
|
||||||
|
|
||||||
// tap gesture
|
// tap gesture
|
||||||
containerView
|
containerView
|
||||||
.publisher(for: UITapGestureRecognizer())
|
.publisher(for: UITapGestureRecognizer())
|
||||||
@ -131,7 +140,7 @@ open class DatePicker: EntryFieldBase {
|
|||||||
hidePopoverView()
|
hidePopoverView()
|
||||||
}
|
}
|
||||||
.store(in: &subscribers)
|
.store(in: &subscribers)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func getFieldContainer() -> UIView {
|
open override func getFieldContainer() -> UIView {
|
||||||
@ -146,7 +155,7 @@ open class DatePicker: EntryFieldBase {
|
|||||||
controlStackView.addArrangedSubview(hiddenView)
|
controlStackView.addArrangedSubview(hiddenView)
|
||||||
return controlStackView
|
return controlStackView
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to make changes to the View based off a change events or from local properties.
|
/// Used to make changes to the View based off a change events or from local properties.
|
||||||
open override func updateView() {
|
open override func updateView() {
|
||||||
super.updateView()
|
super.updateView()
|
||||||
@ -159,7 +168,7 @@ open class DatePicker: EntryFieldBase {
|
|||||||
selectedDateLabel.isEnabled = isEnabled
|
selectedDateLabel.isEnabled = isEnabled
|
||||||
calendarIcon.color = iconColorConfiguration.getColor(self)
|
calendarIcon.color = iconColorConfiguration.getColor(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets to default settings.
|
/// Resets to default settings.
|
||||||
open override func reset() {
|
open override func reset() {
|
||||||
super.reset()
|
super.reset()
|
||||||
@ -172,24 +181,45 @@ open class DatePicker: EntryFieldBase {
|
|||||||
selectedDateLabel.text = formatter.string(from: date)
|
selectedDateLabel.text = formatter.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DatePicker {
|
extension DatePicker {
|
||||||
|
|
||||||
private func showPopover() {
|
private func showPopover() {
|
||||||
guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return }
|
guard let viewController = UIApplication.topViewController(), var parentView = viewController.view, !popoverVisible else {
|
||||||
if popoverVisible {
|
|
||||||
hidePopoverView()
|
hidePopoverView()
|
||||||
} else {
|
return
|
||||||
let calendar = CalendarBase()
|
}
|
||||||
calendar.activeDates = calendarModel.activeDates
|
|
||||||
calendar.hideContainerBorder = calendarModel.hideContainerBorder
|
let calendar = CalendarBase()
|
||||||
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
|
calendar.activeDates = calendarModel.activeDates
|
||||||
calendar.inactiveDates = calendarModel.inactiveDates
|
calendar.hideContainerBorder = calendarModel.hideContainerBorder
|
||||||
calendar.indicators = calendarModel.indicators
|
calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator
|
||||||
calendar.maxDate = calendarModel.maxDate
|
calendar.inactiveDates = calendarModel.inactiveDates
|
||||||
calendar.minDate = calendarModel.minDate
|
calendar.indicators = calendarModel.indicators
|
||||||
calendar.surface = calendarModel.surface
|
calendar.maxDate = calendarModel.maxDate
|
||||||
calendar.setNeedsLayout()
|
calendar.minDate = calendarModel.minDate
|
||||||
calendar.layoutIfNeeded()
|
calendar.surface = calendarModel.surface
|
||||||
|
calendar.setNeedsLayout()
|
||||||
|
calendar.layoutIfNeeded()
|
||||||
|
|
||||||
|
//size the popover
|
||||||
|
popoverViewSize = .init(width: calendar.frame.width, height: calendar.frame.height)
|
||||||
|
|
||||||
|
//find scrollView
|
||||||
|
if scrollView == nil {
|
||||||
|
scrollView = findScrollView(from: containerView)
|
||||||
|
scrollViewContentSize = scrollView?.contentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if let scrollView {
|
||||||
|
parentView = scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
// see if you should use the popover or show an alert
|
||||||
|
if let popoverOrigin = try? calculatePopoverPosition(relativeTo: containerView,
|
||||||
|
in: parentView,
|
||||||
|
size: popoverViewSize,
|
||||||
|
with: popoverSpacing) {
|
||||||
calendar.onChange = { [weak self] control in
|
calendar.onChange = { [weak self] control in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
selectedDate = control.selectedDate
|
selectedDate = control.selectedDate
|
||||||
@ -197,51 +227,28 @@ extension DatePicker {
|
|||||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||||
hidePopoverView()
|
hidePopoverView()
|
||||||
}
|
}
|
||||||
|
|
||||||
outsideTapGesture = UITapGestureRecognizer()
|
// popoverView container
|
||||||
outsidePanGesture = UIPanGestureRecognizer()
|
popoverView = UIView()
|
||||||
|
|
||||||
overlayView.publisher(for: outsideTapGesture!).sink { [weak self] _ in
|
|
||||||
guard let self else { return }
|
|
||||||
hidePopoverView()
|
|
||||||
}.store(in: &subscribers)
|
|
||||||
parentView.publisher(for: outsidePanGesture!).sink { [weak self] _ in
|
|
||||||
guard let self else { return }
|
|
||||||
hidePopoverView()
|
|
||||||
}.store(in: &subscribers)
|
|
||||||
|
|
||||||
overlayView.frame = parentView.bounds
|
|
||||||
overlayView.isHidden = false
|
|
||||||
|
|
||||||
parentView.addSubview(overlayView)
|
|
||||||
|
|
||||||
popoverView = UIScrollView()
|
|
||||||
popoverView.backgroundColor = .green
|
|
||||||
popoverView.clipsToBounds = true
|
|
||||||
popoverView.backgroundColor = .clear
|
popoverView.backgroundColor = .clear
|
||||||
popoverView.isHidden = true
|
popoverView.frame = CGRect(x: popoverOrigin.x, y: popoverOrigin.y, width: calendar.frame.width, height: calendar.frame.height)
|
||||||
popoverView.addSubview(calendar)
|
|
||||||
calendar.pinToSuperView()
|
|
||||||
parentView.addSubview(popoverView)
|
|
||||||
|
|
||||||
|
|
||||||
let spacing: CGFloat = 4
|
|
||||||
let popoverSize: CGSize = .init(width: calendar.frame.width, height: calendar.frame.height)
|
|
||||||
popoverView.contentSize = CGSize(width: popoverSize.width, height: popoverSize.height)
|
|
||||||
|
|
||||||
let (popoverX, popoverY, adjustedHeight) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: popoverSize, with: spacing)
|
|
||||||
//let adjustedX = adjustedHeight != popoverSize.height ? popoverX - 10 : popoverX
|
|
||||||
let adjustedWidth = adjustedHeight != popoverSize.height ? popoverSize.width + 10 : popoverSize.width
|
|
||||||
popoverView.frame = CGRect(x: popoverX, y: popoverY, width: adjustedWidth, height: adjustedHeight)
|
|
||||||
|
|
||||||
parentView.layoutIfNeeded()
|
|
||||||
popoverView.alpha = 0
|
popoverView.alpha = 0
|
||||||
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
popoverView.isHidden = false
|
|
||||||
popoverVisible = true
|
popoverVisible = true
|
||||||
|
popoverView.addSubview(calendar)
|
||||||
|
|
||||||
|
calendar.pinToSuperView()
|
||||||
|
|
||||||
|
// add views
|
||||||
|
parentView.addSubview(popoverView)
|
||||||
|
parentView.layoutIfNeeded()
|
||||||
|
|
||||||
|
// update containerview
|
||||||
_ = responder?.becomeFirstResponder()
|
_ = responder?.becomeFirstResponder()
|
||||||
updateContainerView()
|
updateContainerView()
|
||||||
UIView.animate(withDuration: 0.3,
|
|
||||||
|
// animate the calendar to show
|
||||||
|
UIView.animate(withDuration: 0.5,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
usingSpringWithDamping: 0.8,
|
usingSpringWithDamping: 0.8,
|
||||||
initialSpringVelocity: 0.2,
|
initialSpringVelocity: 0.2,
|
||||||
@ -250,48 +257,87 @@ extension DatePicker {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
popoverView.alpha = 1
|
popoverView.alpha = 1
|
||||||
popoverView.transform = CGAffineTransform.identity
|
popoverView.transform = CGAffineTransform.identity
|
||||||
if popoverSize.height > adjustedHeight {
|
|
||||||
popoverView.flashScrollIndicators()
|
|
||||||
}
|
|
||||||
UIAccessibility.post(notification: .layoutChanged, argument: calendar)
|
UIAccessibility.post(notification: .layoutChanged, argument: calendar)
|
||||||
parentView.layoutIfNeeded()
|
parentView.layoutIfNeeded()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
} else {
|
||||||
|
let dialog = UIScrollView()
|
||||||
private func hidePopoverView() {
|
dialog.translatesAutoresizingMaskIntoConstraints = false
|
||||||
overlayView.isHidden = true
|
dialog.addSubview(calendar)
|
||||||
overlayView.removeFromSuperview()
|
dialog.backgroundColor = .clear
|
||||||
outsideTapGesture = nil
|
dialog.contentSize = .init(width: calendar.frame.width + 20, height: calendar.frame.width + 20)
|
||||||
outsidePanGesture = nil
|
dialog.width(calendar.frame.width + 20)
|
||||||
UIView.animate(withDuration: 0.2,
|
dialog.height(calendar.frame.height + 20)
|
||||||
animations: {[weak self] in
|
calendar.pinToSuperView(.uniform(10))
|
||||||
guard let self else { return }
|
calendar.onChange = { [weak self] control in
|
||||||
popoverView.alpha = 0
|
guard let self else { return }
|
||||||
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
selectedDate = control.selectedDate
|
||||||
}) { [weak self] _ in
|
sendActions(for: .valueChanged)
|
||||||
guard let self else { return }
|
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||||
popoverView.isHidden = true
|
viewController.dismiss(animated: true)
|
||||||
popoverView.removeFromSuperview()
|
}
|
||||||
popoverVisible = false
|
|
||||||
responder?.resignFirstResponder()
|
let alert = AlertViewController().with {
|
||||||
setNeedsUpdate()
|
$0.dialog = dialog
|
||||||
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
$0.modalPresentationStyle = .overCurrentContext
|
||||||
|
$0.modalTransitionStyle = .crossDissolve
|
||||||
|
}
|
||||||
|
topViewController = viewController
|
||||||
|
viewController.present(alert, animated: true){
|
||||||
|
dialog.flashScrollIndicators()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat, CGFloat) {
|
private func hidePopoverView() {
|
||||||
|
if topViewController != nil {
|
||||||
|
topViewController?.dismiss(animated: true)
|
||||||
|
topViewController = nil
|
||||||
|
} else {
|
||||||
|
UIView.animate(withDuration: 0.2,
|
||||||
|
animations: {[weak self] in
|
||||||
|
guard let self, let popoverView else { return }
|
||||||
|
popoverView.alpha = 0
|
||||||
|
popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
|
if let scrollView, let scrollViewContentSize {
|
||||||
|
scrollView.contentSize = scrollViewContentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
}) { [weak self] _ in
|
||||||
|
guard let self, let popoverView else { return }
|
||||||
|
popoverView.isHidden = true
|
||||||
|
popoverView.removeFromSuperview()
|
||||||
|
popoverVisible = false
|
||||||
|
responder?.resignFirstResponder()
|
||||||
|
setNeedsUpdate()
|
||||||
|
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findScrollView(from view: UIView) -> UIScrollView? {
|
||||||
|
var currentView = view
|
||||||
|
while let superview = currentView.superview {
|
||||||
|
if let scrollView = superview as? UIScrollView {
|
||||||
|
return scrollView
|
||||||
|
}
|
||||||
|
currentView = superview
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) throws -> CGPoint? {
|
||||||
let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView)
|
let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView)
|
||||||
let parentBounds = parentView.bounds
|
let parentBounds = parentView.bounds
|
||||||
let safeAreaInsets = parentView.safeAreaInsets
|
let safeAreaInsets = parentView.safeAreaInsets
|
||||||
let popoverWidth = size.width
|
let popoverWidth = size.width
|
||||||
let popoverHeight = size.height
|
let popoverHeight = size.height
|
||||||
|
|
||||||
var popoverX: CGFloat = 0
|
var popoverX: CGFloat = 0
|
||||||
var popoverY: CGFloat = 0
|
var popoverY: CGFloat = 0
|
||||||
var adjustedHeight = popoverHeight
|
|
||||||
|
|
||||||
// Calculate horizontal position
|
// Calculate horizontal position
|
||||||
if sourceFrameInParent.width < popoverWidth {
|
if sourceFrameInParent.width < popoverWidth {
|
||||||
if sourceFrameInParent.midX - popoverWidth / 2 < 0 {
|
if sourceFrameInParent.midX - popoverWidth / 2 < 0 {
|
||||||
@ -307,35 +353,51 @@ extension DatePicker {
|
|||||||
} else {
|
} else {
|
||||||
popoverX = sourceFrameInParent.midX - popoverWidth / 2
|
popoverX = sourceFrameInParent.midX - popoverWidth / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the popover is within the parent's bounds horizontally
|
// Ensure the popover is within the parent's bounds horizontally
|
||||||
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
|
popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth))
|
||||||
|
|
||||||
// Calculate vertical position and height
|
|
||||||
let availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing
|
|
||||||
let availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing
|
|
||||||
let totalAvailableHeight = parentBounds.height - safeAreaInsets.top - safeAreaInsets.bottom
|
|
||||||
|
|
||||||
if availableSpaceAbove >= popoverHeight {
|
var availableSpaceAbove: CGFloat = 0.0
|
||||||
// Show above without adjusting height
|
var availableSpaceBelow: CGFloat = 0.0
|
||||||
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
|
||||||
} else if availableSpaceBelow >= popoverHeight {
|
/// if the scrollView is set we want to change how we calculate the containerView's position
|
||||||
// Show below without adjusting height
|
if var scrollView = parentView as? UIScrollView {
|
||||||
popoverY = sourceFrameInParent.maxY + spacing
|
// Calculate vertical position and height
|
||||||
|
availableSpaceAbove = sourceFrameInParent.minY - scrollView.bounds.minY - spacing
|
||||||
|
availableSpaceBelow = scrollView.bounds.maxY - sourceFrameInParent.maxY - spacing
|
||||||
|
|
||||||
} else if totalAvailableHeight >= popoverHeight {
|
|
||||||
// check if the total
|
|
||||||
if availableSpaceAbove > availableSpaceBelow {
|
if availableSpaceAbove > availableSpaceBelow {
|
||||||
popoverY = safeAreaInsets.top
|
// Show above
|
||||||
|
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
||||||
} else {
|
} else {
|
||||||
popoverY = parentBounds.height - safeAreaInsets.bottom - popoverHeight
|
// Show below
|
||||||
|
popoverY = sourceFrameInParent.maxY + spacing
|
||||||
|
|
||||||
|
// See if we need to expand the contentSize of the ScrollView
|
||||||
|
let diff = scrollView.contentSize.height - sourceFrameInParent.maxY
|
||||||
|
if diff < popoverHeight {
|
||||||
|
scrollView.contentSize.height += popoverHeight - diff + VDSLayout.space4X
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
popoverY = safeAreaInsets.top
|
// Calculate vertical position and height
|
||||||
adjustedHeight = totalAvailableHeight
|
availableSpaceAbove = sourceFrameInParent.minY - safeAreaInsets.top - spacing
|
||||||
|
availableSpaceBelow = parentBounds.height - sourceFrameInParent.maxY - safeAreaInsets.bottom - spacing
|
||||||
|
|
||||||
|
if availableSpaceAbove >= popoverHeight {
|
||||||
|
// Show above
|
||||||
|
popoverY = sourceFrameInParent.minY - popoverHeight - spacing
|
||||||
|
|
||||||
|
} else if availableSpaceBelow >= popoverHeight {
|
||||||
|
// Show below
|
||||||
|
popoverY = sourceFrameInParent.maxY + spacing
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (popoverX, popoverY, adjustedHeight)
|
return .init(x: popoverX, y: popoverY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user