diff --git a/.circleci/config.yml b/.circleci/config.yml index e4c9ab21..69c219d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: diff --git a/.swift-version b/.swift-version index bf77d549..819e07a2 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2 +5.0 diff --git a/.swiftlint.yml b/.swiftlint.yml index 8bf8a3da..5ebf9e77 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aba7f2d..684b8da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cartfile b/Cartfile index 50814158..5e9a95f2 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "nathantannar4/InputBarAccessoryView" "4.2.1" +github "nathantannar4/InputBarAccessoryView" ~> 4.3.0 diff --git a/Cartfile.private b/Cartfile.private index 70f82c40..29d169d6 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,2 +1,2 @@ github "Quick/Quick" ~> 2.0.0 -github "Quick/Nimble" ~> 8.0.1 +github "Quick/Nimble" ~> 8.0.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index de18a3b8..04ee5dc4 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -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" diff --git a/Documentation/CUSTOM_CELLS.md b/Documentation/CUSTOM_CELLS.md new file mode 100644 index 00000000..bdff909a --- /dev/null +++ b/Documentation/CUSTOM_CELLS.md @@ -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) + } +} +``` diff --git a/Example/.Podfile.un~ b/Example/.Podfile.un~ deleted file mode 100644 index 8e635e87..00000000 Binary files a/Example/.Podfile.un~ and /dev/null differ diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 86d703d4..89a85340 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -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; diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme index e34206a1..31f17b89 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme @@ -1,6 +1,6 @@ 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 diff --git a/Example/Podfile~ b/Example/Podfile~ deleted file mode 100644 index bf44d23b..00000000 --- a/Example/Podfile~ +++ /dev/null @@ -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 diff --git a/Example/Sources/Data Generation/Lorem.swift b/Example/Sources/Data Generation/Lorem.swift index 47597730..b368af71 100755 --- a/Example/Sources/Data Generation/Lorem.swift +++ b/Example/Sources/Data Generation/Lorem.swift @@ -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.. "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 diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 4b6ecfac..72509f16 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -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; }; diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme index edb000ac..1f6e5d93 100644 --- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme +++ b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme @@ -1,6 +1,6 @@ - -

-

- +

+ + + A community-driven replacement for JSQMessagesViewController https://messagekit.github.io

[![CircleCI](https://circleci.com/gh/MessageKit/MessageKit.svg?style=svg)](https://circleci.com/gh/MessageKit/MessageKit) @@ -29,6 +28,10 @@ alt="Contributions Welcome"> +

+ +

+ ## 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) diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index c3b96310..2abbd030 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -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, diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index fefe53ee..9a02f20b 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -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) } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 0e154c4f..e74b0183 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -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() { diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index 2cca400d..ea13d297 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -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 { diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index b3cadbd1..f5112a3d 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -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) } diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift index 4025dac0..e0fe3760 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -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) diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index 3c09bbcb..57f74578 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -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 + } + } } + diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index a3fc6153..bd938be8 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -24,8 +24,6 @@ import Foundation -// swiftlint:disable explicit_acl - internal extension UIEdgeInsets { var vertical: CGFloat { diff --git a/Sources/Extensions/UIImage+Extension.swift b/Sources/Extensions/UIImage+Extension.swift index 157fd0f4..5195a1e7 100644 --- a/Sources/Extensions/UIImage+Extension.swift +++ b/Sources/Extensions/UIImage+Extension.swift @@ -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 ?? "") diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index ec01877f..42bbbbd9 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -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 [] diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 4cca7e1c..2b95457f 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -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) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index b5e76738..95ac736d 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -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 diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift index 60219d1d..c62c99fe 100644 --- a/Sources/Models/AvatarPosition.swift +++ b/Sources/Models/AvatarPosition.swift @@ -94,5 +94,5 @@ public extension AvatarPosition { static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal } - + } diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index d2b602a8..1b711a77 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -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` diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift index a1bf6740..91461fa9 100644 --- a/Sources/Models/HorizontalEdgeInsets.swift +++ b/Sources/Models/HorizontalEdgeInsets.swift @@ -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 } } diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift index 28468785..c44b8be1 100644 --- a/Sources/Models/Sender.swift +++ b/Sources/Models/Sender.swift @@ -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) } diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index dd07f5d6..db2d7118 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -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) {} diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 29d43e80..70b72fc5 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -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] { diff --git a/Sources/Supporting/MessageInputBar.swift b/Sources/Supporting/MessageInputBar.swift index 575edd50..7c6f7dbf 100644 --- a/Sources/Supporting/MessageInputBar.swift +++ b/Sources/Supporting/MessageInputBar.swift @@ -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 -// } -//} diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index 6563143e..31a7d363 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -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 diff --git a/Sources/Views/Cells/AudioMessageCell.swift b/Sources/Views/Cells/AudioMessageCell.swift index a9251ce5..b08d7ad5 100644 --- a/Sources/Views/Cells/AudioMessageCell.swift +++ b/Sources/Views/Cells/AudioMessageCell.swift @@ -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" } diff --git a/Sources/Views/Cells/ContactMessageCell.swift b/Sources/Views/Cells/ContactMessageCell.swift index d7f50087..2273a834 100644 --- a/Sources/Views/Cells/ContactMessageCell.swift +++ b/Sources/Views/Cells/ContactMessageCell.swift @@ -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 }() diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index d7e1967b..6363e989 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -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 + } + } + } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index 9fdfefeb..629cc272 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -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) } diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 3ea89490..8ae68e86 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -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) } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 22e75474..05515be2 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -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) } } diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift index 60872b37..ff007c3d 100644 --- a/Sources/Views/TypingIndicator.swift +++ b/Sources/Views/TypingIndicator.swift @@ -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 } } diff --git a/Tests/ControllersTest/MessagesViewControllerSpec.swift b/Tests/ControllersTest/MessagesViewControllerSpec.swift index 77c4de4c..5717fb32 100644 --- a/Tests/ControllersTest/MessagesViewControllerSpec.swift +++ b/Tests/ControllersTest/MessagesViewControllerSpec.swift @@ -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 diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift index 640bbacd..c5f9f67e 100644 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ b/Tests/ControllersTest/MessagesViewControllerTests.swift @@ -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], diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 6cbfb632..717d154e 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift @@ -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() { diff --git a/Tests/ViewsTests/AvatarViewTests.swift b/Tests/ViewsTests/AvatarViewTests.swift index e919f141..602632c7 100644 --- a/Tests/ViewsTests/AvatarViewTests.swift +++ b/Tests/ViewsTests/AvatarViewTests.swift @@ -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() { diff --git a/Tests/ViewsTests/MessagesCollectionViewTests.swift b/Tests/ViewsTests/MessagesCollectionViewTests.swift index a42fa54d..31d515f0 100644 --- a/Tests/ViewsTests/MessagesCollectionViewTests.swift +++ b/Tests/ViewsTests/MessagesCollectionViewTests.swift @@ -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) } }