Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ef615083 | |||
| 8275176f21 | |||
| 4e028ee4e9 | |||
| e925415429 | |||
| 36a19cba83 | |||
| 17da6c00e4 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
language: swift
|
||||
osx_image: xcode10.1
|
||||
osx_image: xcode10.2
|
||||
xcode_project: ScrollingContentViewController.xcodeproj
|
||||
xcode_scheme: ScrollingContentViewController
|
||||
xcode_destination: platform=iOS Simulator,OS=12.1,name=iPhone X
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -43,5 +43,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -43,5 +43,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://travis-ci.org/drewolbrich/ScrollingContentViewController)
|
||||
[](http://developer.apple.com/ios)
|
||||
[](https://developer.apple.com/swift)
|
||||
[](https://developer.apple.com/swift)
|
||||
[](LICENSE)
|
||||
[](http://twitter.com/drewolbrich)
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
ScrollingContentViewController makes it easy to create a view controller with a single scrolling content view, or to convert an existing static view controller into one that scrolls. Most importantly, it takes care of several tricky undocumented edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
ScrollingContentViewController makes it easy to create a view controller with a single scrolling content view, or to convert an existing static view controller into one that scrolls. Most importantly, it takes care of several tricky undocumented edge cases involving the keyboard, navigation controllers, and device rotations.
|
||||
|
||||
## Background
|
||||
|
||||
A common UIKit Auto Layout task involves creating a view controller with a fixed layout that is too large to fit older, smaller devices, or devices in landscape orientation, or the area of the screen that remains visible when the keyboard is presented. The problem is compounded when [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) is used to support large font sizes.
|
||||
A common UIKit Auto Layout task involves creating a view controller with a fixed layout that is too large to fit older, smaller devices, or devices in landscape orientation, or the area of the screen that remains visible when the keyboard is presented. The problem is compounded when [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) is used to support large font sizes.
|
||||
|
||||
For example, consider this sign up screen, which fits iPhone Xs, but not iPhone SE with a keyboard:
|
||||
|
||||
@@ -33,7 +33,7 @@ For example, consider this sign up screen, which fits iPhone Xs, but not iPhone
|
||||
|
||||
This case can be handled by nesting the view inside a scroll view. You could do this manually in Interface Builder, as described by Apple's [Working with Scroll Views](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html) documentation, but many steps are required. If your view contains text fields, you'll have to write code to adjust the view to compensate for the presented keyboard, as described in [Managing the Keyboard](https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3). However, handling the keyboard robustly is [surprisingly complicated](#keyboard-resize-filtering), especially if your app presents a sequence of screens with keyboards in the context of a navigation controller, or when device orientation support is required.
|
||||
|
||||
To simplify this task, ScrollingContentViewController inserts the scroll view into the view hierarchy for you at run time, along with all necessary Auto Layout constraints.
|
||||
To simplify this task, ScrollingContentViewController inserts the scroll view into the view hierarchy for you at run time, along with all necessary Auto Layout constraints.
|
||||
|
||||
When used in a storyboard, ScrollingContentViewController exposes an outlet called [`contentView`](#contentView) that you connect to the view that you'd like to make scrollable. This may be the view controller's root view or an arbitrary subview. Everything else is taken care of automatically, including responding to keyboard presentation and device orientation changes.
|
||||
|
||||
@@ -57,7 +57,7 @@ github "drewolbrich/ScrollingContentViewController"
|
||||
|
||||
## Usage
|
||||
|
||||
Subclasses of `ScrollingContentViewController` may be configured using [storyboards](#storyboards) or in [code](#code).
|
||||
Subclasses of `ScrollingContentViewController` may be configured using [storyboards](#storyboards) or in [code](#code).
|
||||
|
||||
This library may also be used without subclassing, by composing the helper class `ScrollingContentViewManager` instead. Refer to [Usage Without Subclassing](#usage-without-subclassing).
|
||||
|
||||
@@ -71,9 +71,9 @@ To configure `ScrollingContentViewController` in a storyboard:
|
||||
import ScrollingContentViewController
|
||||
|
||||
class MyViewController: ScrollingContentViewController {
|
||||
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -86,7 +86,7 @@ To configure `ScrollingContentViewController` in a storyboard:
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -105,25 +105,25 @@ To integrate `ScrollingContentViewController` programmatically:
|
||||
import ScrollingContentViewController
|
||||
|
||||
class MyViewController: ScrollingContentViewController {
|
||||
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
2. In your view controller's [`viewDidLoad`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload) method, assign a new view to the [`contentView`](#contentView) property. Add all of your controls to this view instead of referencing the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property so they can scroll freely. The view controller's root view referenced by its [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property now acts as a background view behind the scrolling content view.
|
||||
2. In your view controller's [`viewDidLoad`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload) method, assign a new view to the [`contentView`](#contentView) property. Add all of your controls to this view instead of referencing the [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property so they can scroll freely. The view controller's root view referenced by its [`view`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621460-view) property now acts as a background view behind the scrolling content view.
|
||||
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
contentView = UIView()
|
||||
|
||||
|
||||
// Add all controls to contentView instead of view.
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
You may also assign [`contentView`](#contentView) to a subview of your view controller's root view, in which case only that subview will be made scrollable.
|
||||
|
||||
## Caveats
|
||||
@@ -174,7 +174,7 @@ If you make changes to your content view that modify its size, you must call the
|
||||
For example, after updating the view's [`NSLayoutConstraint.constant`](https://developer.apple.com/documentation/uikit/nslayoutconstraint/1526928-constant) properties, you may animate the changes like this:
|
||||
|
||||
```swift
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0,
|
||||
options: [], animations: {
|
||||
self.scrollView.setNeedsLayout()
|
||||
self.scrollView.layoutIfNeeded()
|
||||
@@ -248,7 +248,7 @@ class MyViewController: UIViewController {
|
||||
}
|
||||
```
|
||||
|
||||
The `ScrollingContentViewManager` class supports all of the same [properties](#properties) and [methods](#methods) as `ScrollingContentViewController`.
|
||||
The `ScrollingContentViewManager` class supports all of the same [properties](#view-controller-properties) and [methods](#scroll-view-properties-and-methods) as `ScrollingContentViewController`.
|
||||
|
||||
`ScrollingContentViewManager` can also be used to create a scrolling view controller programatically:
|
||||
|
||||
@@ -263,7 +263,7 @@ class MyViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
// Populate your content view here.
|
||||
// ...
|
||||
|
||||
@@ -309,7 +309,7 @@ class MyViewController: UIViewController {
|
||||
|
||||
* [ManagerExample](Examples/ManagerExample) - Example using `ScrollingContentViewManager` and class composition instead of subclassing `ScrollingContentViewController`.
|
||||
|
||||
* [SequenceExample](Examples/SequenceExample) - Example of a sequence of pushed scrolling view controllers with keyboards in the context of a navigation controller.
|
||||
* [SequenceExample](Examples/SequenceExample) - Example of a sequence of pushed scrolling view controllers with keyboards in the context of a navigation controller.
|
||||
|
||||
* [ReassignExample](Examples/ReassignExample) - Example of dynamically reassigning `contentView`.
|
||||
|
||||
@@ -319,7 +319,7 @@ The `ScrollingContentViewController` and `ScrollingContentViewManager` classes s
|
||||
|
||||
### contentView
|
||||
|
||||
The scrolling content view parented to the scroll view.
|
||||
The scrolling content view parented to the scroll view.
|
||||
|
||||
When this property is first assigned, the view that it references is parented to [`scrollView`](#scrollView), which is then added to the view controller's view hierarchy.
|
||||
|
||||
@@ -327,7 +327,7 @@ If the content view already has a superview, the scroll view replaces it in the
|
||||
|
||||
If the content view has no superview, the scroll view is parented to the view controller's root view and its frame and autoresizing mask are defined to track the root view's bounds.
|
||||
|
||||
If the [`contentView`](#contentView) property is later reassigned, the new content view replaces the old one as the subview of the scroll view, and the scroll view is left otherwise unmodified.
|
||||
If the [`contentView`](#contentView) property is later reassigned, the new content view replaces the old one as the subview of the scroll view, and the scroll view is left otherwise unmodified.
|
||||
|
||||
### scrollView
|
||||
|
||||
@@ -391,7 +391,7 @@ When the content view is first assigned, if it has a superview, the scroll view
|
||||
|
||||
If the content view has no superview, the scroll view is parented to the view controller's root view and its frame and autoresizing mask are defined to track the root view's bounds.
|
||||
|
||||
If the ScrollingContentViewController's `contentView` property references its root view, a new `UIView` is allocated and replaces it as the root view so that the scroll view will have an appropriate view to be parented to.
|
||||
If the ScrollingContentViewController's `contentView` property references its root view, a new `UIView` is allocated and replaces it as the root view so that the scroll view will have an appropriate view to be parented to.
|
||||
|
||||
The content view's superview does not necessarily have to be the view controller's root view, and does not have to match the root view's size.
|
||||
|
||||
@@ -427,7 +427,7 @@ In addition to [keyboard resize filtering](#keyboard-resize-filtering), above, S
|
||||
|
||||
### Navigation Controllers
|
||||
|
||||
ScrollingContentViewController correctly handles sequences of pushed view controllers in the context of a navigation controller, in particular in the case when each view controller calls a text field's [`becomeFirstResponder`](https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder) method in [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear), such that the keyboard remains visible across view controller transitions.
|
||||
ScrollingContentViewController correctly handles sequences of pushed view controllers in the context of a navigation controller, in particular in the case when each view controller calls a text field's [`becomeFirstResponder`](https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder) method in [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear), such that the keyboard remains visible across view controller transitions.
|
||||
|
||||
### Device Orientation Changes
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
3AAC049B21E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049621E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift */; };
|
||||
3AAC049E21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049D21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift */; };
|
||||
3AAC04A021E2F28A00D94DA5 /* ScrollViewBounceControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC049F21E2F28A00D94DA5 /* ScrollViewBounceControlling.swift */; };
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObservering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */; };
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */; };
|
||||
3AAC04A421E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04A321E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift */; };
|
||||
3AAC04AD21E3A01900D94DA5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAC04AC21E3A01900D94DA5 /* AppDelegate.swift */; };
|
||||
3AAC04BC21E3A07500D94DA5 /* ScrollingContentViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5702CE21E2CBB600E4CC55 /* ScrollingContentViewController.framework */; };
|
||||
@@ -228,7 +228,7 @@
|
||||
3AAC049621E2F01C00D94DA5 /* AdditionalSafeAreaInsetsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalSafeAreaInsetsController.swift; sourceTree = "<group>"; };
|
||||
3AAC049D21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalSafeAreaInsetsControlling.swift; sourceTree = "<group>"; };
|
||||
3AAC049F21E2F28A00D94DA5 /* ScrollViewBounceControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewBounceControlling.swift; sourceTree = "<group>"; };
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardObservering.swift; sourceTree = "<group>"; };
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardObserving.swift; sourceTree = "<group>"; };
|
||||
3AAC04A321E301C400D94DA5 /* ScrollViewFilterKeyboardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewFilterKeyboardDelegate.swift; sourceTree = "<group>"; };
|
||||
3AAC04AA21E3A01900D94DA5 /* StoryboardExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StoryboardExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3AAC04AC21E3A01900D94DA5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -369,7 +369,7 @@
|
||||
3AAC049021E2D4F100D94DA5 /* ScrollingContentViewManager.swift */,
|
||||
3ADD380B21EBFE8D00396B7A /* ScrollingContentScrollView.swift */,
|
||||
3AAC049421E2F01C00D94DA5 /* KeyboardObserver.swift */,
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObservering.swift */,
|
||||
3AAC04A121E2F30700D94DA5 /* KeyboardObserving.swift */,
|
||||
3AD597D221F420ED00F220A0 /* KeyboardNotificationManager.swift */,
|
||||
3AD597D421F4221B00F220A0 /* KeyboardNotificationObserving.swift */,
|
||||
3AF3AA3621FACCAF008AF677 /* ScrollViewFilter.swift */,
|
||||
@@ -808,7 +808,7 @@
|
||||
3AAC04A021E2F28A00D94DA5 /* ScrollViewBounceControlling.swift in Sources */,
|
||||
3AAC049E21E2F18E00D94DA5 /* AdditionalSafeAreaInsetsControlling.swift in Sources */,
|
||||
3AD597D521F4221B00F220A0 /* KeyboardNotificationObserving.swift in Sources */,
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObservering.swift in Sources */,
|
||||
3AAC04A221E2F30700D94DA5 /* KeyboardObserving.swift in Sources */,
|
||||
3AD597D321F420ED00F220A0 /* KeyboardNotificationManager.swift in Sources */,
|
||||
3AF3AA3B21FCB76E008AF677 /* ScrollRectEvent.swift in Sources */,
|
||||
3A06577C2200A552005BE8CC /* IsUnitTest.swift in Sources */,
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class KeyboardObserver: NSObject {
|
||||
|
||||
// See https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW3
|
||||
|
||||
private weak var delegate: KeyboardObservering?
|
||||
private weak var delegate: KeyboardObserving?
|
||||
|
||||
private weak var scrollViewFilter: ScrollViewFilter?
|
||||
|
||||
@@ -31,7 +31,7 @@ internal class KeyboardObserver: NSObject {
|
||||
/// calls to `updateForCurrentKeyboardVisibility`.
|
||||
private var isAdjustingViewForKeyboardFrameEvent = false
|
||||
|
||||
init(scrollViewFilter: ScrollViewFilter, delegate: KeyboardObservering) {
|
||||
init(scrollViewFilter: ScrollViewFilter, delegate: KeyboardObserving) {
|
||||
super.init()
|
||||
|
||||
self.scrollViewFilter = scrollViewFilter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// KeyboardObservering.swift
|
||||
// KeyboardObserving.swift
|
||||
// ScrollingContentViewController
|
||||
//
|
||||
// Created by Drew Olbrich on 1/6/19.
|
||||
@@ -11,7 +11,7 @@
|
||||
import UIKit
|
||||
|
||||
/// Delegate for `KeyboardObserver`.
|
||||
internal protocol KeyboardObservering: class {
|
||||
internal protocol KeyboardObserving: class {
|
||||
|
||||
/// View controller over top of which the keyboard is presented.
|
||||
var hostViewController: UIViewController? { get }
|
||||
@@ -19,7 +19,7 @@ import UIKit
|
||||
/// `ScrollingContentViewManager` may be used instead.
|
||||
///
|
||||
/// See [https://github.com/drewolbrich/ScrollingContentViewController](https://github.com/drewolbrich/ScrollingContentViewController/blob/master/README.md) for full documentation.
|
||||
public class ScrollingContentViewManager: KeyboardObservering, ScrollViewBounceControlling, AdditionalSafeAreaInsetsControlling {
|
||||
public class ScrollingContentViewManager: KeyboardObserving, ScrollViewBounceControlling, AdditionalSafeAreaInsetsControlling {
|
||||
|
||||
/// The view controller that hosts the scroll view.
|
||||
public private(set) weak var hostViewController: UIViewController?
|
||||
|
||||
@@ -118,10 +118,11 @@ class InsetContentViewKeyboardTests: XCTestCase {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = true
|
||||
|
||||
let initialContentViewSize = scrollView.safeAreaLayoutGuide.layoutFrame.size
|
||||
let initialBottomSafeAreaInset = rootView.safeAreaInsets.bottom - tabBarHeight
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight))
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight) + initialBottomSafeAreaInset)
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
@@ -105,12 +105,13 @@ class KeyboardTests: XCTestCase {
|
||||
scrollingContentViewManager.shouldResizeContentViewForKeyboard = true
|
||||
|
||||
let initialContentViewSize = rootView.bounds.inset(by: rootView.safeAreaInsets).size
|
||||
let initialBottomSafeAreaInset = rootView.safeAreaInsets.bottom - tabBarHeight
|
||||
|
||||
presentKeyboard()
|
||||
|
||||
// The size of the expected safe area of the view controller's root view after the
|
||||
// keyboard is presented.
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight))
|
||||
let expectedContentViewSize = CGSize(width: initialContentViewSize.width, height: initialContentViewSize.height - (keyboardHeight - tabBarHeight) + initialBottomSafeAreaInset)
|
||||
|
||||
XCTAssertEqual(contentView.frame.size, expectedContentViewSize)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user