mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
Merge pull request #1250 from MessageKit/development
Prepare release 3.1.0-beta.1, migrate to Swift 5
This commit is contained in:
@@ -8,11 +8,11 @@ jobs:
|
||||
build-and-test:
|
||||
|
||||
environment:
|
||||
- DESTINATION: "platform=iOS Simulator,name=iPhone XS"
|
||||
- DESTINATION: "platform=iOS Simulator,name=iPhone 11 Pro"
|
||||
|
||||
# Specify the Xcode version to use.
|
||||
macos:
|
||||
xcode: "10.0.0"
|
||||
xcode: "11.3.1"
|
||||
|
||||
# Define the steps required to build the project.
|
||||
steps:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.2
|
||||
5.0
|
||||
|
||||
@@ -12,7 +12,6 @@ custom_rules:
|
||||
message: "Use like open override or public override instead" # violation message. optional.
|
||||
severity: warning # violation severity. optional.
|
||||
opt_in_rules:
|
||||
- explicit_acl
|
||||
- explicit_top_level_acl
|
||||
explicit_acl: error
|
||||
explicit_top_level_acl: error
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
|
||||
The changelog for `MessageKit`. Also see the [releases](https://github.com/MessageKit/MessageKit/releases) on GitHub.
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set the proper notification to invalidate layout. MessageKit now relies on `UIApplication` orientation notification instead of `UIDevice`, which invalidates the layout only when it is needed. [#1126](https://github.com/MessageKit/MessageKit/pull/1126) by [@bguidolim](https://github.com/bguidolim)
|
||||
|
||||
- Fixed `requiredInitialScrollViewBottomInset` when `inputAccessoryView` is `nil` [#1218](https://github.com/MessageKit/MessageKit/pull/1218) by [@aabosh](https://github.com/aabosh)
|
||||
|
||||
### Added
|
||||
|
||||
- Add missing textAlignment and textInsets assignments to layoutCellTopLabel method in MessageContentCell. [#1117](https://github.com/MessageKit/MessageKit/pull/1117) by [@mdescalzo](https://github.com/mdescalzo)
|
||||
|
||||
- Add support for styling NSLinkAttribute with existing urlAttributes in MessageLabel. [#1091](https://github.com/MessageKit/MessageKit/pull/1091) by [@marcetcheverry](https://github.com/marcetcheverry)
|
||||
|
||||
- Add loading indicator to AudioMessageCell. [#1084](https://github.com/MessageKit/MessageKit/pull/1084) by [@marcetcheverry](https://github.com/marcetcheverry)
|
||||
|
||||
- Lazily initialize the MessageInputBar on MessagesViewController. [#1092](https://github.com/MessageKit/MessageKit/pull/1092) by [@marcetcheverry](https://github.com/marcetcheverry)
|
||||
|
||||
- Add support for Dark Mode [#1189](https://github.com/MessageKit/MessageKit/pull/1189) by [@Vlada31R](https://github.com/Vlada31R)
|
||||
|
||||
- Add support for `scrollToLastItem` and `scrollsToLastItemOnKeyboardBeginsEditing` [#1247](https://github.com/MessageKit/MessageKit/pull/1247) by [@youuu](https://github.com/youuu)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking Change** Updated to Swift 5.0 [#1039](https://github.com/MessageKit/MessageKit/pull/1039) by [@nathantannar4](https://github.com/nathantannar4)
|
||||
|
||||
- Fixes scrollToBottom method to properly handle calls made early in the view lifecycle. [#1110](https://github.com/MessageKit/MessageKit/pull/1110) by [@marcetcheverry](https://github.com/marcetcheverry)
|
||||
|
||||
- Add missing cellTopLabelAlignment to MessageSizeCalculator. [#1113](https://github.com/MessageKit/MessageKit/pull/1113) by [@marcetcheverry](https://github.com/marcetcheverry)
|
||||
|
||||
- **Breaking Change** `MessageInputBar`, and `MessageInputBarDelegate` have been obsoleted. Use`InputBarAccessoryView` and `InputBarAccessoryViewDelegate` respectively. This change was previously meant for 3.0.0 but was implemented erroneously. [#1201](https://github.com/MessageKit/MessageKit/pull/1201) by [@kinoroy](https://github.com/kinoroy)
|
||||
|
||||
- Deprecated `SenderType.id` in favour of `SenderType.senderId`. This change was previously meant for 3.0.0 but
|
||||
was implemented erroneously. [#1201](https://github.com/MessageKit/MessageKit/pull/1201) by [@kinoroy](https://github.com/kinoroy)
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Dependency Changes
|
||||
|
||||
@@ -1 +1 @@
|
||||
github "nathantannar4/InputBarAccessoryView" "4.2.1"
|
||||
github "nathantannar4/InputBarAccessoryView" ~> 4.3.0
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
github "Quick/Quick" ~> 2.0.0
|
||||
github "Quick/Nimble" ~> 8.0.1
|
||||
github "Quick/Nimble" ~> 8.0.0
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
github "Quick/Nimble" "v7.3.4"
|
||||
github "Quick/Quick" "v1.3.4"
|
||||
github "nathantannar4/InputBarAccessoryView" "4.2.2"
|
||||
github "Quick/Nimble" "v8.0.5"
|
||||
github "Quick/Quick" "v2.2.0"
|
||||
github "nathantannar4/InputBarAccessoryView" "4.3.2"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# MessageKit Custom Cell Guide(s)
|
||||
|
||||
- [How can I add a custom cell?](#how-can-i-add-a-custom-cell)
|
||||
|
||||
## How can I add a custom cell?
|
||||
|
||||
**Note:** If you choose to use the `.custom` kind you are responsible for all of the cell's layout. You can design the cell in code or Interface Builder. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not affect your custom cell, even if you subclass your cell from `MessageContentCell`.
|
||||
|
||||
**Creating a custom cell involves four parts:**
|
||||
1. Build a cell in Interface Builder or code that inherits from `UICollectionViewCell`
|
||||
2. Set the size of your cell. Subclass `MessageSizeCalculator` if you want your cell to have the default MessageKit layout design. Subclass `CellSizeCalculator` if you want to further customize your own cell design. The implementation of this class will allow your custom cell to automatically size itself within the `messagesCollectionView`.
|
||||
3. Add your custom cell size to the collection view flow layout. Subclass `MessagesCollectionViewFlowLayout`, and use the custom message size calculator from step 2, above.
|
||||
4. Register your custom cell and reference your custom collection view flow layout.
|
||||
|
||||
|
||||
### Example:
|
||||
|
||||
Let's take a look at what it takes to create a custom cell that displays a red block for your custom message.
|
||||
|
||||
- **1. Custom Cell**: We create a cell that inherits from `UICollectionViewCell`
|
||||
**MyCustomCell.swift**
|
||||
```swift
|
||||
open class MyCustomCell: UICollectionViewCell {
|
||||
open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
|
||||
self.contentView.backgroundColor = UIColor.red
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **2. MessageSizeCalculator**: We set the size of our cell by creating a class that inherits from `MessageSizeCalculator`
|
||||
**CustomMessageSizeCalculator.swift**
|
||||
```swift
|
||||
open class CustomMessageSizeCalculator: MessageSizeCalculator {
|
||||
open override func messageContainerSize(for message: MessageType) -> CGSize {
|
||||
// Customize this function implementation to size your content appropriately. This example simply returns a constant size
|
||||
// Refer to the default MessageKit cell implementations, and the Example App to see how to size a custom cell dynamically
|
||||
return CGSize(width: 300, height: 130)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **3. MessageFlowLayout**: We add our custom message size calculator to our collection view layout by creating a class that inherits from `MessagesCollectionViewFlowLayout`
|
||||
**MyCustomMessagesFlowLayout.swift**
|
||||
```swift
|
||||
open class MyCustomMessagesFlowLayout: MessagesCollectionViewFlowLayout {
|
||||
lazy open var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self)
|
||||
|
||||
override open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
if case .custom = message.kind {
|
||||
return customMessageSizeCalculator
|
||||
}
|
||||
return super.cellSizeCalculatorForItem(at: indexPath);
|
||||
}
|
||||
}
|
||||
```
|
||||
- **4. Implementation**: We register our custom cell and reference our newly created `MyCustomMessagesFlowLayout.swift`
|
||||
**ConversationViewController.swift**
|
||||
```swift
|
||||
internal class ConversationViewController: MessagesViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: MyCustomMessagesFlowLayout())
|
||||
messagesCollectionView.register(MyCustomCell.self)
|
||||
//...
|
||||
}
|
||||
//...
|
||||
override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError("Ouch. nil data source for messages")
|
||||
}
|
||||
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
if case .custom = message.kind {
|
||||
let cell = messagesCollectionView.dequeueReusableCell(MyCustomCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
}
|
||||
return super.collectionView(collectionView, cellForItemAt: indexPath)
|
||||
}
|
||||
}
|
||||
```
|
||||
Binary file not shown.
@@ -371,7 +371,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0730;
|
||||
LastUpgradeCheck = 0930;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = MessageKit;
|
||||
TargetAttributes = {
|
||||
882B5E321CF7D4B900B6E160 = {
|
||||
@@ -391,7 +391,7 @@
|
||||
};
|
||||
buildConfigurationList = 882B5E2E1CF7D4B900B6E160 /* Build configuration list for PBXProject "ChatExample" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -523,7 +523,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
|
||||
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -603,6 +603,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -651,7 +652,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -660,6 +661,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -700,7 +702,7 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
@@ -710,14 +712,13 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A830E27DBE0B66B89C5D2EB8 /* Pods-ChatExample.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -725,7 +726,6 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9E0D67CD75BA7EB323FD391B /* Pods-ChatExample.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist";
|
||||
@@ -733,7 +733,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -746,7 +746,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -761,7 +761,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample";
|
||||
};
|
||||
name = Release;
|
||||
@@ -774,7 +774,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = ChatExample;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -788,7 +788,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = ChatExample;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
source 'https://cdn.cocoapods.org/'
|
||||
|
||||
platform :ios, '9.0'
|
||||
|
||||
target 'ChatExample' do
|
||||
|
||||
+10
-8
@@ -1,13 +1,15 @@
|
||||
PODS:
|
||||
- InputBarAccessoryView (4.2.2)
|
||||
- MessageKit (3.0.0-beta-swift4-2):
|
||||
- InputBarAccessoryView
|
||||
- InputBarAccessoryView (4.3.1):
|
||||
- InputBarAccessoryView/Core (= 4.3.1)
|
||||
- InputBarAccessoryView/Core (4.3.1)
|
||||
- MessageKit (3.1.0-beta.1):
|
||||
- InputBarAccessoryView (~> 4.3.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- MessageKit (from `../`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
trunk:
|
||||
- InputBarAccessoryView
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -15,9 +17,9 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
InputBarAccessoryView: 2b937602598e2fab3149f37f51dd7ad795653812
|
||||
MessageKit: feb3ca4fa02a14f3391781323ced1da84a0aa73c
|
||||
InputBarAccessoryView: 58a348be7ea2736c7eec60e5c315511c2dbb39fd
|
||||
MessageKit: 6b809a162328346e0727dbcd040b5f9fd5f5ba15
|
||||
|
||||
PODFILE CHECKSUM: cecdb7bc8129cf99f66de9f68eea3256fec30c3d
|
||||
PODFILE CHECKSUM: 90bd12ca685503630d7fe716609f4974660ed5ee
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
COCOAPODS: 1.8.4
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
platform :ios, '9.0'
|
||||
|
||||
target 'ChatExample' do
|
||||
use_frameworks!
|
||||
pod 'MessageKit', :path => '../'
|
||||
pod 'MessageInputBar', :git => 'https://github.com/MessageKit/MessageInputBar.git', :branch => 'master'
|
||||
|
||||
target 'ChatExampleTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
target 'ChatExampleUITests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
end
|
||||
@@ -222,7 +222,7 @@ public extension Array {
|
||||
/**
|
||||
Shuffle the array in-place using the Fisher-Yates algorithm.
|
||||
*/
|
||||
public mutating func shuffle() {
|
||||
mutating func shuffle() {
|
||||
for i in 0..<(count - 1) {
|
||||
let j = Int(arc4random_uniform(UInt32(count - i))) + i
|
||||
if j != i {
|
||||
@@ -237,7 +237,7 @@ public extension Array {
|
||||
|
||||
- returns: Returns a shuffled version of the array.
|
||||
*/
|
||||
public func shuffled() -> [Element] {
|
||||
func shuffled() -> [Element] {
|
||||
var list = self
|
||||
list.shuffle()
|
||||
|
||||
@@ -249,7 +249,7 @@ public extension Array {
|
||||
- returns: Returns a random element from the array or `nil` if the
|
||||
array is empty.
|
||||
*/
|
||||
public func random() -> Element? {
|
||||
func random() -> Element? {
|
||||
return (count > 0) ? self.shuffled()[0] : nil
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ public extension Array {
|
||||
Return a random subset of `cnt` elements from the array.
|
||||
- returns: Returns a random subset of `cnt` elements from the array.
|
||||
*/
|
||||
public func random(_ count: Int = 1) -> [Element] {
|
||||
func random(_ count: Int = 1) -> [Element] {
|
||||
let result = shuffled()
|
||||
|
||||
return (count > result.count) ? result : Array(result[0..<count])
|
||||
|
||||
@@ -195,6 +195,10 @@ extension ChatViewController: MessageCellDelegate {
|
||||
print("Message tapped")
|
||||
}
|
||||
|
||||
func didTapImage(in cell: MessageCollectionViewCell) {
|
||||
print("Image tapped")
|
||||
}
|
||||
|
||||
func didTapCellTopLabel(in cell: MessageCollectionViewCell) {
|
||||
print("Top cell label tapped")
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'MessageKit'
|
||||
s.version = '3.0.0'
|
||||
s.version = '3.1.0-beta.1'
|
||||
s.license = { :type => "MIT", :file => "LICENSE.md" }
|
||||
|
||||
s.summary = 'An elegant messages UI library for iOS.'
|
||||
@@ -12,16 +12,16 @@ Pod::Spec.new do |s|
|
||||
s.source_files = 'Sources/**/*.swift'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
"SWIFT_VERSION" => "4.2",
|
||||
"SWIFT_VERSION" => "5.0",
|
||||
}
|
||||
|
||||
s.swift_version = '4.2'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
s.ios.deployment_target = '9.0'
|
||||
s.ios.resource_bundle = { 'MessageKitAssets' => 'Assets/MessageKitAssets.bundle/Images' }
|
||||
|
||||
s.requires_arc = true
|
||||
|
||||
s.dependency 'InputBarAccessoryView', '~> 4.2.2'
|
||||
s.dependency 'InputBarAccessoryView', '~> 4.3.0'
|
||||
|
||||
end
|
||||
|
||||
@@ -563,7 +563,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0730;
|
||||
LastUpgradeCheck = 1010;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = MessageKit;
|
||||
TargetAttributes = {
|
||||
88916B211CF0DF2F00469F91 = {
|
||||
@@ -577,11 +577,11 @@
|
||||
};
|
||||
buildConfigurationList = 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 88916B181CF0DF2F00469F91;
|
||||
productRefGroup = 88916B231CF0DF2F00469F91 /* Products */;
|
||||
@@ -788,7 +788,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -841,7 +841,7 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -871,7 +871,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -897,7 +897,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1010"
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<p>
|
||||
<img src="https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/mklogo.png" title="MessageKit logo">
|
||||
</p>
|
||||
<p>
|
||||
<img src="https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/TypingIndicator.png" title="MessageKit header">
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/mklogo.png" title="MessageKit logo" width="400">
|
||||
|
||||
A community-driven replacement for JSQMessagesViewController https://messagekit.github.io
|
||||
</p>
|
||||
|
||||
[](https://circleci.com/gh/MessageKit/MessageKit)
|
||||
@@ -29,6 +28,10 @@
|
||||
alt="Contributions Welcome">
|
||||
</a>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/TypingIndicator.png" title="MessageKit header" width="400">
|
||||
</p>
|
||||
|
||||
## Goals
|
||||
|
||||
- Provide a :rotating_light:safe:rotating_light: environment for others to learn and grow through Open Source.
|
||||
@@ -43,11 +46,14 @@ See [VISION.md](https://github.com/MessageKit/MessageKit/blob/master/VISION.md)
|
||||
## Installation
|
||||
### [CocoaPods](https://cocoapods.org/) **Recommended**
|
||||
````ruby
|
||||
# Swift 4.2
|
||||
# Swift 5.0
|
||||
pod 'MessageKit'
|
||||
````
|
||||
|
||||
> If you are already using Swift 5, use the `3.0.0-swift5` branch until the offical release is made
|
||||
> For Swift 4.2 use version 3.0.0
|
||||
````ruby
|
||||
# Swift 4.2
|
||||
pod 'MessageKit', '~> 3.0.0'
|
||||
````
|
||||
|
||||
### [Carthage](https://github.com/Carthage/Carthage)
|
||||
|
||||
@@ -61,9 +67,8 @@ github "MessageKit/MessageKit"
|
||||
|
||||
## Requirements
|
||||
|
||||
- **iOS9** or later
|
||||
- **Swift 4.2** or later
|
||||
|
||||
- **iOS 9** or later
|
||||
- **Swift 5.0** or later
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -120,6 +125,7 @@ public enum MessageKind {
|
||||
```
|
||||
|
||||
If you choose to use the `.custom` kind you are responsible for all of the cells layout. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not effect your custom cell. Even if you subclass your cell from `MessageContentCell`.
|
||||
[Read more about custom cells](https://github.com/MessageKit/MessageKit/blob/master/Documentation/CUSTOM_CELLS.md)
|
||||
|
||||
[Read more about the cases on the Quick Start guide.](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md#messagekind)
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ internal extension MessagesViewController {
|
||||
|
||||
// MARK: - Register / Unregister Observers
|
||||
|
||||
internal func addKeyboardObservers() {
|
||||
func addKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: UITextView.textDidBeginEditingNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewTopInset), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
internal func removeKeyboardObservers() {
|
||||
func removeKeyboardObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
@@ -45,9 +45,15 @@ internal extension MessagesViewController {
|
||||
|
||||
@objc
|
||||
private func handleTextViewDidBeginEditing(_ notification: Notification) {
|
||||
if scrollsToBottomOnKeyboardBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return }
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
if scrollsToLastItemOnKeyboardBeginsEditing || scrollsToBottomOnKeyboardBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView,
|
||||
inputTextView === messageInputBar.inputTextView else { return }
|
||||
|
||||
if scrollsToLastItemOnKeyboardBeginsEditing {
|
||||
messagesCollectionView.scrollToLastItem()
|
||||
} else {
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +68,12 @@ internal extension MessagesViewController {
|
||||
// ignore this notification.
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard self.presentedViewController == nil else {
|
||||
// This is important to skip notifications from child modal controllers in iOS >= 13.0
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Note that the check above does not exclude all notifications from an undocked keyboard, only the weird ones.
|
||||
//
|
||||
// We've tried following Apple's recommended approach of tracking UIKeyboardWillShow / UIKeyboardDidHide and ignoring frame
|
||||
@@ -82,25 +88,25 @@ internal extension MessagesViewController {
|
||||
// We could make it work by adding extra checks for the state of the keyboard and compensating accordingly, but it seems easier
|
||||
// to simply check whether the current keyboard frame, whatever it is (even when undocked), covers the bottom of the collection
|
||||
// view.
|
||||
|
||||
|
||||
guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)
|
||||
|
||||
|
||||
let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame)
|
||||
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
|
||||
|
||||
|
||||
if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
|
||||
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
|
||||
messagesCollectionView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
|
||||
messageCollectionViewBottomInset = newBottomInset
|
||||
}
|
||||
|
||||
// MARK: - Inset Computation
|
||||
|
||||
@objc
|
||||
internal func adjustScrollViewTopInset() {
|
||||
func adjustScrollViewTopInset() {
|
||||
if #available(iOS 11.0, *) {
|
||||
// No need to add to the top contentInset
|
||||
} else {
|
||||
@@ -116,7 +122,7 @@ internal extension MessagesViewController {
|
||||
// we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view;
|
||||
// see https://developer.apple.com/videos/play/wwdc2017/242/ for more details
|
||||
let intersection = messagesCollectionView.frame.intersection(keyboardFrame)
|
||||
|
||||
|
||||
if intersection.isNull || (messagesCollectionView.frame.maxY - intersection.maxY) > 0.001 {
|
||||
// The keyboard is hidden, is a hardware one, or is undocked and does not cover the bottom of the collection view.
|
||||
// Note: intersection.maxY may be less than messagesCollectionView.frame.maxY when dealing with undocked keyboards.
|
||||
@@ -126,9 +132,9 @@ internal extension MessagesViewController {
|
||||
}
|
||||
}
|
||||
|
||||
internal func requiredInitialScrollViewBottomInset() -> CGFloat {
|
||||
guard let inputAccessoryView = inputAccessoryView else { return 0 }
|
||||
return max(0, inputAccessoryView.frame.height + additionalBottomInset - automaticallyAddedBottomInset)
|
||||
func requiredInitialScrollViewBottomInset() -> CGFloat {
|
||||
let inputAccessoryViewHeight = inputAccessoryView?.frame.height ?? 0
|
||||
return max(0, inputAccessoryViewHeight + additionalBottomInset - automaticallyAddedBottomInset)
|
||||
}
|
||||
|
||||
/// iOS 11's UIScrollView can automatically add safe area insets to its contentInset,
|
||||
|
||||
@@ -28,11 +28,11 @@ internal extension MessagesViewController {
|
||||
|
||||
// MARK: - Register / Unregister Observers
|
||||
|
||||
internal func addMenuControllerObservers() {
|
||||
func addMenuControllerObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil)
|
||||
}
|
||||
|
||||
internal func removeMenuControllerObservers() {
|
||||
func removeMenuControllerObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,20 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
|
||||
open var messagesCollectionView = MessagesCollectionView()
|
||||
|
||||
/// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
|
||||
open var messageInputBar = InputBarAccessoryView()
|
||||
open lazy var messageInputBar = InputBarAccessoryView()
|
||||
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
|
||||
/// last item whenever the `InputTextView` begins editing.
|
||||
///
|
||||
/// The default value of this property is `false`.
|
||||
/// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
|
||||
open var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false
|
||||
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
|
||||
/// bottom whenever the `InputTextView` begins editing.
|
||||
///
|
||||
/// The default value of this property is `false`.
|
||||
/// NOTE: This is related to `scrollToBottom` whereas the above flag is related to `scrollToLastItem` - check each function for differences
|
||||
open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false
|
||||
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView`
|
||||
@@ -146,9 +154,10 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
|
||||
private func setupDefaults() {
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
view.backgroundColor = .white
|
||||
view.backgroundColor = .backgroundColor
|
||||
messagesCollectionView.keyboardDismissMode = .interactive
|
||||
messagesCollectionView.alwaysBounceVertical = true
|
||||
messagesCollectionView.backgroundColor = .backgroundColor
|
||||
}
|
||||
|
||||
private func setupDelegates() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import Foundation
|
||||
|
||||
internal extension Bundle {
|
||||
|
||||
static func messageKitAssetBundle() -> Bundle { // swiftlint:disable:this explicit_acl
|
||||
static func messageKitAssetBundle() -> Bundle {
|
||||
let podBundle = Bundle(for: MessagesViewController.self)
|
||||
|
||||
guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else {
|
||||
|
||||
@@ -26,7 +26,7 @@ import Foundation
|
||||
|
||||
internal extension CGRect {
|
||||
|
||||
init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { // swiftlint:disable:this explicit_acl
|
||||
init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) {
|
||||
self.init(x: x, y: y, width: w, height: h)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import Foundation
|
||||
|
||||
internal extension NSAttributedString {
|
||||
|
||||
func width(considering height: CGFloat) -> CGFloat { // swiftlint:disable:this explicit_acl
|
||||
func width(considering height: CGFloat) -> CGFloat {
|
||||
|
||||
let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
|
||||
let rect = self.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
|
||||
|
||||
@@ -24,18 +24,97 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable explicit_acl
|
||||
internal extension UIColor {
|
||||
|
||||
static var incomingGray: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemGray5
|
||||
} else {
|
||||
return UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
static var outgoingGreen: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemGreen
|
||||
} else {
|
||||
return UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
static var inputBarGray: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemGray2
|
||||
} else {
|
||||
return UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
static var playButtonLightGray: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemGray6
|
||||
} else {
|
||||
return UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
static var sendButtonBlue: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return UIColor.systemBlue
|
||||
} else {
|
||||
return UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension UIColor {
|
||||
|
||||
static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0)
|
||||
static var backgroundColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return systemBackground
|
||||
} else {
|
||||
return white
|
||||
}
|
||||
}
|
||||
|
||||
static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0)
|
||||
static var labelColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return label
|
||||
} else {
|
||||
return black
|
||||
}
|
||||
}
|
||||
|
||||
static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0)
|
||||
static var placeholderTextColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return placeholderText
|
||||
} else {
|
||||
return .darkGray
|
||||
}
|
||||
}
|
||||
|
||||
static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0)
|
||||
static var grayColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return .systemGray
|
||||
} else {
|
||||
return gray
|
||||
}
|
||||
}
|
||||
|
||||
static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0)
|
||||
static var darkTextColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return .systemGray
|
||||
} else {
|
||||
return darkText
|
||||
}
|
||||
}
|
||||
|
||||
static var lightGrayColor: UIColor {
|
||||
if #available(iOS 13, *) {
|
||||
return .systemGray5
|
||||
} else {
|
||||
return .lightGray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable explicit_acl
|
||||
|
||||
internal extension UIEdgeInsets {
|
||||
|
||||
var vertical: CGFloat {
|
||||
|
||||
@@ -33,7 +33,7 @@ import UIKit
|
||||
/// This extension provide a way to access image resources with in framework
|
||||
public extension UIImage {
|
||||
|
||||
public class func messageKitImageWith(type: ImageType) -> UIImage? {
|
||||
class func messageKitImageWith(type: ImageType) -> UIImage? {
|
||||
let assetBundle = Bundle.messageKitAssetBundle()
|
||||
let imagePath = assetBundle.path(forResource: type.rawValue, ofType: "png", inDirectory: "Images")
|
||||
let image = UIImage(contentsOfFile: imagePath ?? "")
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
// swiftlint:disable explicit_acl
|
||||
|
||||
internal extension UIView {
|
||||
|
||||
func fillSuperview() {
|
||||
@@ -67,7 +65,7 @@ internal extension UIView {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
internal func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, centerY: NSLayoutYAxisAnchor? = nil, centerX: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, centerYConstant: CGFloat = 0, centerXConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
|
||||
func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, centerY: NSLayoutYAxisAnchor? = nil, centerX: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, centerYConstant: CGFloat = 0, centerXConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] {
|
||||
|
||||
if self.superview == nil {
|
||||
return []
|
||||
|
||||
@@ -78,6 +78,7 @@ open class MessageSizeCalculator: CellSizeCalculator {
|
||||
attributes.messageContainerPadding = messageContainerPadding(for: message)
|
||||
attributes.messageContainerSize = messageContainerSize(for: message)
|
||||
attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath)
|
||||
attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: message)
|
||||
attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath)
|
||||
attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message)
|
||||
attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath)
|
||||
|
||||
@@ -89,7 +89,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
|
||||
private func setupObserver() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Typing Indicator API
|
||||
|
||||
@@ -94,5 +94,5 @@ public extension AvatarPosition {
|
||||
static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool {
|
||||
return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ public enum DetectorType: Hashable {
|
||||
}
|
||||
|
||||
///The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted.
|
||||
public func hash(into: inout Hasher) {
|
||||
into.combine(toInt())
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(toInt())
|
||||
}
|
||||
|
||||
/// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable`
|
||||
|
||||
@@ -40,6 +40,8 @@ public struct HorizontalEdgeInsets: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Equatable Conformance
|
||||
|
||||
public extension HorizontalEdgeInsets {
|
||||
|
||||
static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool {
|
||||
@@ -49,7 +51,7 @@ public extension HorizontalEdgeInsets {
|
||||
|
||||
internal extension HorizontalEdgeInsets {
|
||||
|
||||
internal var horizontal: CGFloat {
|
||||
var horizontal: CGFloat {
|
||||
return left + right
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,17 @@
|
||||
import Foundation
|
||||
|
||||
/// An object that groups the metadata of a messages sender.
|
||||
@available(*, deprecated: 3.0.0, message: "`Sender` has been replaced with the `SenderType` protocol in 3.0.0")
|
||||
@available(*, deprecated, message: "`Sender` has been replaced with the `SenderType` protocol in 3.0.0")
|
||||
public struct Sender: SenderType {
|
||||
|
||||
/// MARK: - Properties
|
||||
// MARK: - Properties
|
||||
|
||||
/// The unique String identifier for the sender.
|
||||
///
|
||||
/// Note: This value must be unique across all senders.
|
||||
public let senderId: String
|
||||
|
||||
@available(*, deprecated: 3.0.0, message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol")
|
||||
@available(*, deprecated, renamed: "senderId", message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol")
|
||||
public var id: String {
|
||||
return senderId
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public struct Sender: SenderType {
|
||||
self.displayName = displayName
|
||||
}
|
||||
|
||||
@available(*, deprecated: 3.0.0, message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol")
|
||||
@available(*, deprecated, message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol")
|
||||
public init(id: String, displayName: String) {
|
||||
self.init(senderId: id, displayName: displayName)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,16 @@ public protocol MessageCellDelegate: MessageLabelDelegate {
|
||||
/// method `messageForItem(at:indexPath:messagesCollectionView)`.
|
||||
func didTapAccessoryView(in cell: MessageCollectionViewCell)
|
||||
|
||||
/// Triggered when a tap occurs on the image.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - cell: The image where the touch occurred.
|
||||
///
|
||||
/// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s
|
||||
/// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource`
|
||||
/// method `messageForItem(at:indexPath:messagesCollectionView)`.
|
||||
func didTapImage(in cell: MessageCollectionViewCell)
|
||||
|
||||
/// Triggered when a tap occurs on the play button from audio cell.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -164,6 +174,8 @@ public extension MessageCellDelegate {
|
||||
func didTapCellBottomLabel(in cell: MessageCollectionViewCell) {}
|
||||
|
||||
func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {}
|
||||
|
||||
func didTapImage(in cell: MessageCollectionViewCell) {}
|
||||
|
||||
func didTapPlayButton(in cell: AudioMessageCell) {}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ public extension MessagesDisplayDelegate {
|
||||
case .emoji:
|
||||
return .clear
|
||||
default:
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .white }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .backgroundColor }
|
||||
return dataSource.isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ public extension MessagesDisplayDelegate {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
return dataSource.isFromCurrentSender(message: message) ? .white : .darkText
|
||||
return dataSource.isFromCurrentSender(message: message) ? .backgroundColor : .labelColor
|
||||
}
|
||||
|
||||
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
|
||||
|
||||
@@ -25,31 +25,8 @@
|
||||
import UIKit
|
||||
import InputBarAccessoryView
|
||||
|
||||
@available(*, obsoleted: 3.0.0, renamed: "InputBarAccessoryView")
|
||||
@available(*, unavailable, renamed: "InputBarAccessoryView")
|
||||
public typealias MessageInputBar = InputBarAccessoryView
|
||||
|
||||
@available(*, obsoleted: 3.0.0, renamed: "InputBarAccessoryViewDelegate")
|
||||
@available(*, unavailable, renamed: "InputBarAccessoryViewDelegate")
|
||||
public typealias MessageInputBarDelegate = InputBarAccessoryViewDelegate
|
||||
|
||||
//public extension MessageInputBarDelegate {
|
||||
//
|
||||
// @available(*, obsoleted: 3.0.0, message: "`MessageInputBar` has been replaced with `InputBarAccessoryView` in 3.0.0. Use `inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String)` instead.")
|
||||
// func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
|
||||
// }
|
||||
//
|
||||
// @available(*, obsoleted: 3.0.0, message: "`MessageInputBar` has been replaced with `InputBarAccessoryView` in 3.0.0. Use `inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String)` instead.")
|
||||
// func messageInputBar(_ inputBar: MessageInputBar, textViewTextDidChangeTo text: String) {
|
||||
// }
|
||||
//
|
||||
// @available(*, obsoleted: 3.0.0, message: "`MessageInputBar` has been replaced with `InputBarAccessoryView` in 3.0.0. Use `inputBar(_ inputBar: InputBarAccessoryView, didChangeIntrinsicContentTo size: CGSize)` instead.")
|
||||
// func messageInputBar(_ inputBar: MessageInputBar, didChangeIntrinsicContentTo size: CGSize) {
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension InputBarButtonItem {
|
||||
//
|
||||
// @available(*, renamed: "inputBarAccessoryView")
|
||||
// public var messageInputBar: MessageInputBar? {
|
||||
// return inputBarAccessoryView
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -161,7 +161,7 @@ open class AvatarView: UIImageView {
|
||||
// MARK: - Internal methods
|
||||
|
||||
internal func prepareView() {
|
||||
backgroundColor = .gray
|
||||
backgroundColor = .grayColor
|
||||
contentMode = .scaleAspectFill
|
||||
layer.masksToBounds = true
|
||||
clipsToBounds = true
|
||||
|
||||
@@ -47,18 +47,26 @@ open class AudioMessageCell: MessageContentCell {
|
||||
return durationLabel
|
||||
}()
|
||||
|
||||
public lazy var activityIndicatorView: UIActivityIndicatorView = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .gray)
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
activityIndicatorView.isHidden = true
|
||||
return activityIndicatorView
|
||||
}()
|
||||
|
||||
public lazy var progressView: UIProgressView = {
|
||||
let progressView = UIProgressView(progressViewStyle: .default)
|
||||
progressView.progress = 0.0
|
||||
return progressView
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
/// Responsible for setting up the constraints of the cell's subviews.
|
||||
open func setupConstraints() {
|
||||
playButton.constraint(equalTo: CGSize(width: 25, height: 25))
|
||||
playButton.addConstraints(left: messageContainerView.leftAnchor, centerY: messageContainerView.centerYAnchor, leftConstant: 5)
|
||||
activityIndicatorView.addConstraints(centerY: playButton.centerYAnchor, centerX: playButton.centerXAnchor)
|
||||
durationLabel.addConstraints(right: messageContainerView.rightAnchor, centerY: messageContainerView.centerYAnchor, rightConstant: 15)
|
||||
progressView.addConstraints(left: playButton.rightAnchor, right: durationLabel.leftAnchor, centerY: messageContainerView.centerYAnchor, leftConstant: 5, rightConstant: 5)
|
||||
}
|
||||
@@ -66,6 +74,7 @@ open class AudioMessageCell: MessageContentCell {
|
||||
open override func setupSubviews() {
|
||||
super.setupSubviews()
|
||||
messageContainerView.addSubview(playButton)
|
||||
messageContainerView.addSubview(activityIndicatorView)
|
||||
messageContainerView.addSubview(durationLabel)
|
||||
messageContainerView.addSubview(progressView)
|
||||
setupConstraints()
|
||||
@@ -75,6 +84,8 @@ open class AudioMessageCell: MessageContentCell {
|
||||
super.prepareForReuse()
|
||||
progressView.progress = 0
|
||||
playButton.isSelected = false
|
||||
activityIndicatorView.stopAnimating()
|
||||
playButton.isHidden = false
|
||||
durationLabel.text = "0:00"
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ open class ContactMessageCell: MessageContentCell {
|
||||
/// The view container that holds contact initials
|
||||
public lazy var initialsContainerView: UIView = {
|
||||
let initialsContainer = UIView(frame: CGRect.zero)
|
||||
initialsContainer.backgroundColor = .white
|
||||
initialsContainer.backgroundColor = .backgroundColor
|
||||
return initialsContainer
|
||||
}()
|
||||
|
||||
@@ -42,7 +42,7 @@ open class ContactMessageCell: MessageContentCell {
|
||||
public lazy var initialsLabel: UILabel = {
|
||||
let initialsLabel = UILabel(frame: CGRect.zero)
|
||||
initialsLabel.textAlignment = .center
|
||||
initialsLabel.textColor = .darkText
|
||||
initialsLabel.textColor = .labelColor
|
||||
initialsLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
|
||||
return initialsLabel
|
||||
}()
|
||||
|
||||
@@ -81,4 +81,17 @@ open class MediaMessageCell: MessageContentCell {
|
||||
|
||||
displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
/// Handle tap gesture on contentView and its subviews.
|
||||
open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
|
||||
let touchLocation = gesture.location(in: self)
|
||||
|
||||
switch true {
|
||||
case imageView.frame.contains(touchLocation):
|
||||
delegate?.didTapImage(in: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -267,6 +267,9 @@ open class MessageContentCell: MessageCollectionViewCell {
|
||||
/// Positions the cell's top label.
|
||||
/// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
|
||||
open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
|
||||
cellTopLabel.textAlignment = attributes.cellTopLabelAlignment.textAlignment
|
||||
cellTopLabel.textInsets = attributes.cellTopLabelAlignment.textInsets
|
||||
|
||||
cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,9 @@ open class MessageLabel: UILabel {
|
||||
|
||||
public static var defaultAttributes: [NSAttributedString.Key: Any] = {
|
||||
return [
|
||||
NSAttributedString.Key.foregroundColor: UIColor.darkText,
|
||||
NSAttributedString.Key.foregroundColor: UIColor.darkTextColor,
|
||||
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
NSAttributedString.Key.underlineColor: UIColor.darkText
|
||||
NSAttributedString.Key.underlineColor: UIColor.darkTextColor
|
||||
]
|
||||
}()
|
||||
|
||||
@@ -273,6 +273,11 @@ open class MessageLabel: UILabel {
|
||||
guard let rangeTuples = rangesForDetectors[detector] else { continue }
|
||||
|
||||
for (range, _) in rangeTuples {
|
||||
// This will enable us to attribute it with our own styles, since `UILabel` does not provide link attribute overrides like `UITextView` does
|
||||
if detector.textCheckingType == .link {
|
||||
mutableAttributedString.removeAttribute(NSAttributedString.Key.link, range: range)
|
||||
}
|
||||
|
||||
let attributes = detectorAttributes(for: detector)
|
||||
mutableAttributedString.addAttributes(attributes, range: range)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ open class MessagesCollectionView: UICollectionView {
|
||||
|
||||
public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
|
||||
super.init(frame: frame, collectionViewLayout: layout)
|
||||
backgroundColor = .white
|
||||
backgroundColor = .backgroundColor
|
||||
registerReusableViews()
|
||||
setupGestureRecognizers()
|
||||
}
|
||||
@@ -100,10 +100,25 @@ open class MessagesCollectionView: UICollectionView {
|
||||
cell?.handleTapGesture(gesture)
|
||||
}
|
||||
|
||||
// NOTE: It's possible for small content size this wouldn't work - https://github.com/MessageKit/MessageKit/issues/725
|
||||
public func scrollToLastItem(at pos: UICollectionView.ScrollPosition = .bottom, animated: Bool = true) {
|
||||
guard numberOfSections > 0 else { return }
|
||||
|
||||
let lastSection = numberOfSections - 1
|
||||
let lastItemIndex = numberOfItems(inSection: lastSection) - 1
|
||||
|
||||
guard lastItemIndex >= 0 else { return }
|
||||
|
||||
let indexPath = IndexPath(row: lastItemIndex, section: lastSection)
|
||||
scrollToItem(at: indexPath, at: pos, animated: animated)
|
||||
}
|
||||
|
||||
// NOTE: This method seems to cause crash in certain cases - https://github.com/MessageKit/MessageKit/issues/725
|
||||
// Could try using `scrollToLastItem` above
|
||||
public func scrollToBottom(animated: Bool = false) {
|
||||
let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height
|
||||
|
||||
performBatchUpdates(nil) { _ in
|
||||
performBatchUpdates(nil) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let collectionViewContentHeight = self.collectionViewLayout.collectionViewContentSize.height
|
||||
self.scrollRectToVisible(CGRect(0.0, collectionViewContentHeight - 1.0, 1.0, 1.0), animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ open class TypingIndicator: UIView {
|
||||
public var bounceOffset: CGFloat = 2.5
|
||||
|
||||
/// A convenience accessor for the `backgroundColor` of each dot
|
||||
open var dotColor: UIColor = UIColor.lightGray {
|
||||
open var dotColor: UIColor = UIColor.lightGrayColor {
|
||||
didSet {
|
||||
dots.forEach { $0.backgroundColor = dotColor }
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ final class MessagesViewControllerSpec: QuickSpec {
|
||||
expect(controller.extendedLayoutIncludesOpaqueBars).to(beTrue())
|
||||
}
|
||||
it("sets the background color to be white") {
|
||||
expect(controller.view.backgroundColor).to(be(UIColor.white))
|
||||
expect(controller.view.backgroundColor).to(be(UIColor.backgroundColor))
|
||||
}
|
||||
it("sets keyboardDismissMode to .interactive") {
|
||||
let dismissMode = controller.messagesCollectionView.keyboardDismissMode
|
||||
|
||||
@@ -105,7 +105,7 @@ class MessagesViewControllerTests: XCTestCase {
|
||||
func testCellForItemWithAttributedTextData_returnsTextMessageCell() {
|
||||
let messagesDataSource = MockMessagesDataSource()
|
||||
sut.messagesCollectionView.messagesDataSource = messagesDataSource
|
||||
let attributes = [NSAttributedString.Key.foregroundColor: UIColor.black]
|
||||
let attributes = [NSAttributedString.Key.foregroundColor: UIColor.labelColor]
|
||||
let attriutedString = NSAttributedString(string: "Test", attributes: attributes)
|
||||
messagesDataSource.messages.append(MockMessage(attributedText: attriutedString,
|
||||
user: messagesDataSource.senders[0],
|
||||
|
||||
@@ -71,7 +71,7 @@ class MessagesDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(backgroundColor, .white)
|
||||
XCTAssertEqual(backgroundColor, .backgroundColor)
|
||||
}
|
||||
|
||||
func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() {
|
||||
@@ -143,7 +143,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .white)
|
||||
XCTAssertEqual(textColor, .backgroundColor)
|
||||
}
|
||||
|
||||
func testTextColorFromYou_returnsDarkTextForDefault() {
|
||||
@@ -151,7 +151,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .darkText)
|
||||
XCTAssertEqual(textColor, .labelColor)
|
||||
}
|
||||
|
||||
func testTextColorWithoutDataSource_returnsDarkTextForDefault() {
|
||||
@@ -161,7 +161,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .darkText)
|
||||
XCTAssertEqual(textColor, .labelColor)
|
||||
}
|
||||
|
||||
func testEnableDetectors_returnsEmptyForDefault() {
|
||||
|
||||
@@ -42,7 +42,9 @@ class AvatarViewTests: XCTestCase {
|
||||
|
||||
func testNoParams() {
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
// For certain dynamic colors, need to compare cgColor in XCTest
|
||||
// https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testWithImage() {
|
||||
@@ -50,7 +52,7 @@ class AvatarViewTests: XCTestCase {
|
||||
avatarView.set(avatar: avatar)
|
||||
XCTAssertEqual(avatar.initials, "?")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testInitialsOnly() {
|
||||
@@ -59,13 +61,13 @@ class AvatarViewTests: XCTestCase {
|
||||
XCTAssertEqual(avatarView.initials, avatar.initials)
|
||||
XCTAssertEqual(avatar.initials, "DL")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testSetBackground() {
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
avatarView.backgroundColor = UIColor.red
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.red)
|
||||
XCTAssertEqual(avatarView.backgroundColor!, UIColor.red)
|
||||
}
|
||||
|
||||
func testGetImage() {
|
||||
|
||||
@@ -44,7 +44,7 @@ class MessagesCollectionViewTests: XCTestCase {
|
||||
func testInit() {
|
||||
XCTAssertEqual(messagesCollectionView.frame, rect)
|
||||
XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout)
|
||||
XCTAssertEqual(messagesCollectionView.backgroundColor, .white)
|
||||
XCTAssertEqual(messagesCollectionView.backgroundColor, .backgroundColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user