diff --git a/.github/workflows/ci_pr_example.yml b/.github/workflows/ci_pr_example.yml index cb3215700..437310337 100644 --- a/.github/workflows/ci_pr_example.yml +++ b/.github/workflows/ci_pr_example.yml @@ -10,12 +10,10 @@ concurrency: jobs: tests: name: Build Example app - runs-on: macos-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + runs-on: macos-14 steps: - name: Checkout the Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 10 - name: Build and run example project diff --git a/.github/workflows/ci_pr_framework.yml b/.github/workflows/ci_pr_framework.yml index b2a79229d..da5365c13 100644 --- a/.github/workflows/ci_pr_framework.yml +++ b/.github/workflows/ci_pr_framework.yml @@ -10,11 +10,9 @@ concurrency: jobs: tests: name: Build Framework - runs-on: macos-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + runs-on: macos-14 steps: - name: Checkout the Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build framework run: make framework diff --git a/.github/workflows/ci_pr_tests.yml b/.github/workflows/ci_pr_tests.yml index e5e35f91a..0c1058452 100644 --- a/.github/workflows/ci_pr_tests.yml +++ b/.github/workflows/ci_pr_tests.yml @@ -10,11 +10,9 @@ concurrency: jobs: tests: name: Run Tests - runs-on: macos-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + runs-on: macos-14 steps: - name: Checkout the Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build and run tests run: make test diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 78ecf1893..000000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Danger - -on: - pull_request_target: - types: [opened, reopened, synchronize, ready_for_review] - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' - cancel-in-progress: true - -jobs: - danger: - name: Run Danger - runs-on: macos-13 - env: - DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer - steps: - - name: Checkout the Git repository - uses: actions/checkout@v3 - with: - fetch-depth: 40 - token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ github.event.pull_request.head.ref }} - - name: Resolve SwiftPM dependencies - run: rm -rf ".build" && swift package clean && swift package resolve - - name: Run build script - run: gem install bundler && bundle install && bundle exec danger --fail-on-errors=true - env: - DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index ac3472478..9648963dd 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -15,10 +15,10 @@ concurrency: jobs: build_docs: - runs-on: macos-latest + runs-on: macos-14 steps: - name: Checkout ๐Ÿ›Ž๏ธ - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build DocC run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e88fa51c..9cdd2b6c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,32 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added ### Fixed + +### Updated + +- Update InputBarAccessoryView to fix some crashes and issues by [@kaspik](https://github.com/Kaspik) ### Changed ### Removed +## 4.3.0 +- Fix for SwiftUI example IBAV position issues (example app) by @Janneman84 in https://github.com/MessageKit/MessageKit/pull/1807 +- Added Coursicle to the list of apps using MessageKit by @monstermac77 in https://github.com/MessageKit/MessageKit/pull/1809 +- duration NaN issue fix by @kkarakamis in https://github.com/MessageKit/MessageKit/pull/1812 +- Add OutyPlay to list of apps using MessageKit by @fabdurso in https://github.com/MessageKit/MessageKit/pull/1820 +- Fix #816 by @RomanPodymov in https://github.com/MessageKit/MessageKit/pull/1819 +- Update Makefile by @Kaspik in https://github.com/MessageKit/MessageKit/pull/1833 +- Added ability to specify additionalBottomSpace for keyboardManager by @Almaz5200 in https://github.com/MessageKit/MessageKit/pull/1821 +- build(deps): Bump rexml from 3.2.5 to 3.2.8 by @dependabot in https://github.com/MessageKit/MessageKit/pull/1839 +- Added listener for keyboard input mode changes (e.g. emoji keyboard) by @raulolmedocheca in https://github.com/MessageKit/MessageKit/pull/1842 +- build(deps): Bump rexml from 3.2.8 to 3.3.6 by @dependabot in https://github.com/MessageKit/MessageKit/pull/1858 +- Fix for overlapping detected matches by @SkiTles55 in https://github.com/MessageKit/MessageKit/pull/1853 +- Fix timestamp label layout when not in fullscreen by @CocoaBob in https://github.com/MessageKit/MessageKit/pull/1854 +- โ™ป๏ธ Rename plugins to avoid clashing with SwiftLintPlugins by @technocidal in https://github.com/MessageKit/MessageKit/pull/1862 +- Added custom image masking by @GitNirajHub in https://github.com/MessageKit/MessageKit/pull/1860 +- Update to Swift 5.10 + ## 4.2.0 ### Added @@ -331,6 +352,11 @@ Version 4.0.0 comes with couple of breaking changes, please refer to [MIGRATION_ ## [2.0.0-beta.1](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0-beta.1) +### Fixed + +- Fixed `boundingRect(with:options:)` miscalculation of `MessageLabel` by using `NSLayoutManager`, like text `แŠห˜ฬดอˆฬ๊ˆŠห˜ฬดอˆฬ€แŠโ‹†โœฉ`ใ€`Tomorrow is the day`. +[#824](https://github.com/MessageKit/MessageKit/pull/824) by [@zhongwuzw](https://github.com/zhongwuzw). + ### Changed - **Breaking Change** Updated codebase to Swift 4.2 [#883](https://github.com/MessageKit/MessageKit/pull/883) by [@nathantannar4](https://github.com/nathantannar4) diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 1f646ec98..10cd6690b 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 7959e015f..3cb9f9d4a 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -36,6 +36,7 @@ final internal class AppDelegate: UIResponder, UIApplicationDelegate { ] : [masterViewController] splitViewController.preferredDisplayMode = .allVisible + masterViewController.navigationItem.largeTitleDisplayMode = .never window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = splitViewController diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json new file mode 100644 index 000000000..307a7038d --- /dev/null +++ b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Group 1272628320.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 1272628320@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 1272628320@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png new file mode 100644 index 000000000..faaad6d77 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png new file mode 100644 index 000000000..bdbde8f5a Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png new file mode 100644 index 000000000..156e9f5c5 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png differ diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index 00875b7a2..9f1994faa 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -67,7 +67,11 @@ extension BasicExampleViewController: MessagesDisplayDelegate { func messageStyle(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle { let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft - return .bubbleTail(tail, .curved) + if let image = UIImage(named: "bobbly") { + return .customImageTail(image, tail) + } else { + return .bubbleTail(tail, .curved) + } } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) { diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index f42f903df..aeb2171ae 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -52,7 +52,6 @@ class ChatViewController: MessagesViewController, MessagesDataSource { override func viewDidLoad() { super.viewDidLoad() - navigationItem.largeTitleDisplayMode = .never navigationItem.title = "MessageKit" configureMessageCollectionView() @@ -333,7 +332,7 @@ extension ChatViewController: InputBarAccessoryViewDelegate { let substring = attributedText.attributedSubstring(from: range) let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil) - print("Autocompleted: `", substring, "` with context: ", context ?? []) + print("Autocompleted: `", substring, "` with context: ", context ?? "-") } let components = inputBar.inputTextView.components diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index cb25821dd..90c42b894 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -42,7 +42,6 @@ final internal class LaunchViewController: UITableViewController { super.viewDidLoad() title = "MessageKit" navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - navigationController?.navigationBar.prefersLargeTitles = true navigationController?.navigationBar.tintColor = .primaryColor tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift index 91ee8ff4e..3fd11c7e0 100644 --- a/Example/Sources/View Controllers/MessageContainerController.swift +++ b/Example/Sources/View Controllers/MessageContainerController.swift @@ -44,11 +44,6 @@ final class MessageContainerController: UIViewController { conversationViewController.canBecomeFirstResponder } - /// Required for the `MessageInputBar` to be visible - override var inputAccessoryView: UIView? { - conversationViewController.inputAccessoryView - } - override func viewDidLoad() { super.viewDidLoad() @@ -62,18 +57,6 @@ final class MessageContainerController: UIViewController { view.addSubview(bannerView) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isTranslucent = true - navigationController?.navigationBar.barTintColor = .clear - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.isTranslucent = false - navigationController?.navigationBar.barTintColor = .primaryColor - } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let headerHeight: CGFloat = 200 diff --git a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift index 808019a92..ed6e116c0 100644 --- a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift +++ b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift @@ -39,16 +39,4 @@ final class MessageSubviewContainerViewController: UIViewController { view.addSubview(messageSubviewViewController.view) messageSubviewViewController.didMove(toParent: self) } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isTranslucent = true - navigationController?.navigationBar.barTintColor = .clear - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.isTranslucent = false - navigationController?.navigationBar.barTintColor = .primaryColor - } } diff --git a/Example/Sources/View Controllers/MessageSubviewViewController.swift b/Example/Sources/View Controllers/MessageSubviewViewController.swift index 4f7e74f7f..04d128891 100644 --- a/Example/Sources/View Controllers/MessageSubviewViewController.swift +++ b/Example/Sources/View Controllers/MessageSubviewViewController.swift @@ -56,7 +56,5 @@ final class MessageSubviewViewController: BasicExampleViewController { // MARK: Private - private var keyboardManager = KeyboardManager() - private let subviewInputBar = InputBarAccessoryView() } diff --git a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift index 12c69c028..b4fd02bc0 100644 --- a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift +++ b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift @@ -23,9 +23,20 @@ struct SwiftUIExampleView: View { self.cleanupSocket() } .navigationBarTitle("SwiftUI Example", displayMode: .inline) + .modifier(IgnoresSafeArea()) //fixes issue with IBAV placement when keyboard appears } // MARK: Private + + private struct IgnoresSafeArea: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 14.0, *) { + content.ignoresSafeArea(.keyboard, edges: .bottom) + } else { + content + } + } + } private func connectToMessageSocket() { MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]).onNewMessage { message in diff --git a/Gemfile.lock b/Gemfile.lock index 1ab5ad023..7af99e226 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,11 +70,13 @@ GEM public_suffix (5.0.1) rake (13.0.6) rchardet (1.8.0) - rexml (3.2.5) + rexml (3.3.6) + strscan ruby2_keywords (0.0.5) sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) + strscan (3.1.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thor (0.20.3) diff --git a/Makefile b/Makefile index 0343d308b..87ab6715a 100644 --- a/Makefile +++ b/Makefile @@ -25,15 +25,15 @@ test: @echo "Running MessageKit tests." - @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 14" | xcpretty -c + @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 15" | xcpretty -c framework: @echo "Building MessageKit Framework." - @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 14" | xcpretty -c + @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 15" | xcpretty -c build_example: @echo "Building & testing MessageKit Example app." - @cd Example && set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 14" CODE_SIGNING_REQUIRED=NO | xcpretty -c + @cd Example && set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 15" CODE_SIGNING_REQUIRED=NO | xcpretty -c format: @swift package --allow-writing-to-package-directory format-source-code --file . diff --git a/Package.swift b/Package.swift index 0ed44c8fb..b0e0f6747 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.10 // MIT License // @@ -27,11 +27,11 @@ let package = Package( platforms: [.iOS(.v13)], products: [ .library(name: "MessageKit", targets: ["MessageKit"]), - .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"]), + .plugin(name: "LintPlugin", targets: ["LintPlugin"]), .plugin(name: "SwiftFormatPlugin", targets: ["SwiftFormatPlugin"]), ], dependencies: [ - .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "6.1.0")), + .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "6.5.0")), ], targets: [ // MARK: - MessageKit @@ -48,24 +48,24 @@ let package = Package( // MARK: - Plugins .binaryTarget( - name: "SwiftLintBinary", + name: "LintBinary", url: "https://github.com/realm/SwiftLint/releases/download/0.47.1/SwiftLintBinary-macos.artifactbundle.zip", checksum: "82ef90b7d76b02e41edd73423687d9cedf0bb9849dcbedad8df3a461e5a7b555" ), .plugin( - name: "SwiftLintPlugin", + name: "LintPlugin", capability: .buildTool(), - dependencies: ["SwiftLintBinary"] + dependencies: ["LintBinary"] ), .plugin( - name: "SwiftLintCommandPlugin", + name: "LintCommandPlugin", capability: .command( intent: .custom( verb: "lint", description: "Lint Swift source files" ) ), - dependencies: ["SwiftLintBinary"] + dependencies: ["LintBinary"] ), .binaryTarget( diff --git a/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift b/Plugins/LintCommandPlugin/SwiftLintCommandPlugin.swift similarity index 100% rename from Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift rename to Plugins/LintCommandPlugin/SwiftLintCommandPlugin.swift diff --git a/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift b/Plugins/LintPlugin/SwiftLintPlugin.swift similarity index 97% rename from Plugins/SwiftLintPlugin/SwiftLintPlugin.swift rename to Plugins/LintPlugin/SwiftLintPlugin.swift index 174d2a0f2..e88b10abd 100644 --- a/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift +++ b/Plugins/LintPlugin/SwiftLintPlugin.swift @@ -21,7 +21,7 @@ import PackagePlugin @main -struct SwiftLintPlugin: BuildToolPlugin { +struct LintPlugin: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { [ .buildCommand( diff --git a/README.md b/README.md index 5313b0350..d34412a50 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Great! Look over these things first. - Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md) - Check the [Contributing Guide Lines](https://github.com/MessageKit/MessageKit/blob/master/CONTRIBUTING.md). -- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA) and ๐Ÿ—ฃ don't be a stranger. +- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/zt-2484ymok0-O82~1EtnHALSngQvn6Xwyw) and ๐Ÿ—ฃ don't be a stranger. - Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those. - Download the project and check out the current code base. Suggest any improvements by opening a new issue. - Check out the [What's Next](#whats-next) section :point_down: to see where we are headed. @@ -154,6 +154,7 @@ Interested in contributing to MessageKit? Click here to join our [Slack](https:/ Add your app to the list of apps using this library and make a pull request. - [ClassDojo](https://www.classdojo.com) +- [Coursicle](https://apps.apple.com/us/app/coursicle/id1187418307) - [Connect Messaging](https://apps.apple.com/app/id1607268774) - [Ring4](https://www.ring4.com) - [Formacar](https://itunes.apple.com/ru/app/id1180117334) @@ -173,6 +174,7 @@ Add your app to the list of apps using this library and make a pull request. - [XPASS](https://apps.apple.com/cz/app/id1596773834) - [HeiaHeia](https://www.heiaheia.com) - [Starstruck AI](https://apps.apple.com/au/app/starstruck-message-anyone/id6446234281) +- [OutyPlay](https://apps.apple.com/app/id6450551793) _Please provide attribution, it is greatly appreciated._ diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 90ab8efa3..974fa1077 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -31,7 +31,10 @@ extension MessagesViewController { // MARK: - Register Observers internal func addKeyboardObservers() { - keyboardManager.bind(inputAccessoryView: inputContainerView) + keyboardManager.bind( + inputAccessoryView: inputContainerView, + withAdditionalBottomSpace: { [weak self] in self?.inputBarAdditionalBottomSpace() ?? 0 } + ) keyboardManager.bind(to: messagesCollectionView) /// Observe didBeginEditing to scroll content to last item if necessary @@ -66,6 +69,21 @@ extension MessagesViewController { } .store(in: &disposeBag) + NotificationCenter.default + .publisher(for: UITextInputMode.currentInputModeDidChangeNotification) + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly + .sink { [weak self] _ in + self?.updateMessageCollectionViewBottomInset() + + if !(self?.maintainPositionOnInputBarHeightChanged ?? false) { + self?.messagesCollectionView.scrollToLastItem() + } + } + .store(in: &disposeBag) + /// Observe frame change of the input bar container to update collectioView bottom inset inputContainerView.publisher(for: \.center) .receive(on: DispatchQueue.main) diff --git a/Sources/Controllers/MessagesViewController+State.swift b/Sources/Controllers/MessagesViewController+State.swift index a00700980..d72c3d186 100644 --- a/Sources/Controllers/MessagesViewController+State.swift +++ b/Sources/Controllers/MessagesViewController+State.swift @@ -40,7 +40,7 @@ extension MessagesViewController { // MARK: - Getters - var keyboardManager: KeyboardManager { state.keyboardManager } + public var keyboardManager: KeyboardManager { state.keyboardManager } var panGesture: UIPanGestureRecognizer? { get { state.panGesture } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 7d21bf7bd..1046517d5 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -66,6 +66,11 @@ open class MessagesViewController: UIViewController, UICollectionViewDelegateFlo } } + /// withAdditionalBottomSpace parameter for InputBarAccessoryView's KeyboardManager + open func inputBarAdditionalBottomSpace() -> CGFloat { + 0 + } + open override func viewDidLoad() { super.viewDidLoad() setupDefaults() diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 1d9065c9e..099487215 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -316,15 +316,33 @@ open class MessageSizeCalculator: CellSizeCalculator { } // MARK: Internal + internal lazy var textContainer: NSTextContainer = { + let textContainer = NSTextContainer() + textContainer.maximumNumberOfLines = 0 + textContainer.lineFragmentPadding = 0 + return textContainer + }() + internal lazy var layoutManager: NSLayoutManager = { + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + return layoutManager + }() + internal lazy var textStorage: NSTextStorage = { + let textStorage = NSTextStorage() + textStorage.addLayoutManager(layoutManager) + return textStorage + }() internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) - let rect = attributedText.boundingRect( - with: constraintBox, - options: [.usesLineFragmentOrigin, .usesFontLeading], - context: nil).integral - return rect.size + textContainer.size = constraintBox + textStorage.replaceCharacters(in: NSRange(location: 0, length: textStorage.length), with: attributedText) + layoutManager.ensureLayout(for: textContainer) + + let size = layoutManager.usedRect(for: textContainer).size + + return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up)) } } diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index 7fbbbd212..fcdbbf60f 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -30,6 +30,7 @@ public enum MessageStyle { case bubbleOutline(UIColor) case bubbleTail(TailCorner, TailStyle) case bubbleTailOutline(UIColor, TailCorner, TailStyle) + case customImageTail(UIImage,TailCorner) case custom((MessageContainerView) -> Void) // MARK: Public @@ -75,6 +76,20 @@ public enum MessageStyle { { return cachedImage } + + func strechAndCache(image: UIImage) -> UIImage { + let stretchedImage = stretch(image) + if let imageCacheKey = imageCacheKey { + MessageStyle.bubbleImageCache.setObject(stretchedImage, forKey: imageCacheKey as NSString) + } + return stretchedImage + } + + if case .customImageTail(let image, let corner) = self { + guard let cgImage = image.cgImage else { return nil } + let image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) + return strechAndCache(image: image) + } guard let imageName = imageName, @@ -86,18 +101,14 @@ public enum MessageStyle { switch self { case .none, .custom: return nil - case .bubble, .bubbleOutline: + case .bubble, .bubbleOutline, .customImageTail: break case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _): guard let cgImage = image.cgImage else { return nil } image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) } - let stretchedImage = stretch(image) - if let imageCacheKey = imageCacheKey { - MessageStyle.bubbleImageCache.setObject(stretchedImage, forKey: imageCacheKey as NSString) - } - return stretchedImage + return strechAndCache(image: image) } // MARK: Internal @@ -133,7 +144,7 @@ public enum MessageStyle { return "bubble_full" + tailStyle.imageNameSuffix case .bubbleTailOutline(_, _, let tailStyle): return "bubble_outlined" + tailStyle.imageNameSuffix - case .none, .custom: + case .none, .custom, .customImageTail: return nil } } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index b012d7851..2548fde8f 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -385,7 +385,7 @@ extension MessagesDisplayDelegate { returnValue = String(format: "0:%.02d", Int(duration.rounded(.up))) } else if duration < 3600 { returnValue = String(format: "%.02d:%.02d", Int(duration / 60), Int(duration) % 60) - } else { + } else if duration.isFinite { let hours = Int(duration / 3600) let remainingMinutesInSeconds = Int(duration) - hours * 3600 returnValue = String( diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index 034180d5a..471f36c3b 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -365,9 +365,8 @@ open class MessageContentCell: MessageCollectionViewCell { open func layoutTimeLabelView(with attributes: MessagesCollectionViewLayoutAttributes) { let paddingLeft: CGFloat = 10 let origin = CGPoint( - x: UIScreen.main.bounds.width + paddingLeft, - y: messageContainerView.frame.minY + messageContainerView.frame.height * 0.5 - messageTimestampLabel - .font.ascender * 0.5) + x: self.frame.maxX + paddingLeft, + y: messageContainerView.frame.minY + messageContainerView.frame.height * 0.5 - messageTimestampLabel.font.ascender * 0.5) let size = CGSize(width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height) messageTimestampLabel.frame = CGRect(origin: origin, size: size) } diff --git a/Sources/Views/MessageContainerView.swift b/Sources/Views/MessageContainerView.swift index bcc5e3a64..e816f0d19 100644 --- a/Sources/Views/MessageContainerView.swift +++ b/Sources/Views/MessageContainerView.swift @@ -49,14 +49,14 @@ open class MessageContainerView: UIImageView { switch style { case .none, .custom: break - case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline: + case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline, .customImageTail: imageMask.frame = bounds } } private func applyMessageStyle() { switch style { - case .bubble, .bubbleTail: + case .bubble, .bubbleTail, .customImageTail: imageMask.image = style.image sizeMaskToView() mask = imageMask diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index feb9c95a6..036bc3565 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -130,7 +130,7 @@ open class MessageLabel: UILabel { open override func drawText(in rect: CGRect) { let insetRect = rect.inset(by: textInsets) - textContainer.size = CGSize(width: insetRect.width, height: rect.height) + textContainer.size = CGSize(width: insetRect.width, height: insetRect.height) let origin = insetRect.origin let range = layoutManager.glyphRange(for: textContainer) @@ -363,7 +363,7 @@ open class MessageLabel: UILabel { .filter { $0.isCustom } .map { parseForMatches(with: $0, in: text, for: range) } .joined() - matches.append(contentsOf: regexs) + matches.append(contentsOf: removeOverlappingResults(Array(regexs))) // Get all Checking Types of detectors, except for .custom because they contain their own regex let detectorCheckingTypes = enabledDetectors @@ -402,6 +402,28 @@ open class MessageLabel: UILabel { fatalError("You must pass a .custom DetectorType") } } + + private func removeOverlappingResults(_ results: [NSTextCheckingResult]) -> [NSTextCheckingResult] + { + var filteredResults: [NSTextCheckingResult] = [] + + for result in results { + let overlappingResults = results.filter { $0.range.intersection(result.range)?.length ?? 0 > 0 } + + if overlappingResults.count <= 1 { + filteredResults.append(result) + continue + } + + guard !filteredResults.contains(where: { $0.range == result.range }) else { continue } + let maxRangeResult = overlappingResults.max { $0.range.upperBound - $0.range.lowerBound < $1.range.upperBound - $1.range.lowerBound } + if let maxRangeResult { + filteredResults.append(maxRangeResult) + } + } + + return filteredResults + } private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) { guard checkingResults.isEmpty == false else { return } diff --git a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift index 77f4eb647..4d1264768 100644 --- a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift +++ b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift @@ -82,6 +82,21 @@ final class MessageLabelTests: XCTestCase { let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel) XCTAssertEqual(invalidMatches.count, 0) } + + func testCustomDetectionOverlapping() { + let testText = "address MNtz8Zz1cPD1CZadoc38jT5qeqeFBS6Aif can match multiple regex's" + + let messageLabel = MessageLabel() + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Custom"): "CustomDetected"] + let detectors = [ + DetectorType.custom(try! NSRegularExpression(pattern: "(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}", options: .caseInsensitive)), + DetectorType.custom(try! NSRegularExpression(pattern: #"([3ML][\w]{26,33})|ltc1[\w]+"#, options: .caseInsensitive)), + DetectorType.custom(try! NSRegularExpression(pattern: "[qmN][a-km-zA-HJ-NP-Z1-9]{26,33}", options: .caseInsensitive))] + + set(text: testText, and: detectors, with: attributes, to: messageLabel) + let matches = detectors.map { extractCustomDetectors(for: $0, with: messageLabel) }.joined() + XCTAssertEqual(matches.count, 1) + } func testSyncBetweenAttributedAndText() { let messageLabel = MessageLabel()