Void) {
+ var messages: [MockMessage] = []
+ // Enable Custom Messages
+ UserDefaults.standard.set(true, forKey: "Custom Messages")
+ for _ in 0 ..< count {
+ let message = randomMessage(allowedSenders: senders)
+ messages.append(message)
}
+ completion(messages)
+ }
- func getAvatarFor(sender: SenderType) -> Avatar {
- let firstName = sender.displayName.components(separatedBy: " ").first
- let lastName = sender.displayName.components(separatedBy: " ").first
- let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")"
- switch sender.senderId {
- case "000001":
- return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials)
- case "000002":
- return Avatar(image: #imageLiteral(resourceName: "Steven-Deutsch"), initials: initials)
- case "000003":
- return Avatar(image: #imageLiteral(resourceName: "Wu-Zhong"), initials: initials)
- case "000000":
- return Avatar(image: nil, initials: "SS")
- default:
- return Avatar(image: nil, initials: initials)
- }
+ func getMessages(count: Int, allowedSenders _: [MockUser], completion: ([MockMessage]) -> Void) {
+ var messages: [MockMessage] = []
+ // Disable Custom Messages
+ UserDefaults.standard.set(false, forKey: "Custom Messages")
+ for _ in 0 ..< count {
+ let uniqueID = UUID().uuidString
+ let user = senders.random()!
+ let date = dateAddingRandomTime()
+ let randomSentence = Lorem.sentence()
+ let message = MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date)
+ messages.append(message)
}
+ completion(messages)
+ }
+ func getAvatarFor(sender: SenderType) -> Avatar {
+ let firstName = sender.displayName.components(separatedBy: " ").first
+ let lastName = sender.displayName.components(separatedBy: " ").first
+ let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")"
+ switch sender.senderId {
+ case "000001":
+ return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials)
+ case "000002":
+ return Avatar(image: #imageLiteral(resourceName: "Steven-Deutsch"), initials: initials)
+ case "000003":
+ return Avatar(image: #imageLiteral(resourceName: "Wu-Zhong"), initials: initials)
+ case "000000":
+ return Avatar(image: nil, initials: "SS")
+ default:
+ return Avatar(image: nil, initials: initials)
+ }
+ }
}
diff --git a/Example/Sources/Extensions/AlertService.swift b/Example/Sources/Extensions/AlertService.swift
new file mode 100644
index 000000000..58d3b3c2c
--- /dev/null
+++ b/Example/Sources/Extensions/AlertService.swift
@@ -0,0 +1,27 @@
+//
+// AlertService.swift
+// ChatExample
+//
+// Created by Mohannad on 12/25/20.
+// Copyright © 2020 MessageKit. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+class AlertService {
+ static func showAlert(
+ style: UIAlertController.Style,
+ title: String?,
+ message: String?,
+ actions: [UIAlertAction] = [UIAlertAction(title: "Ok", style: .cancel, handler: nil)],
+ completion: (() -> Swift.Void)? = nil)
+ {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: style)
+ for action in actions {
+ alert.addAction(action)
+ }
+
+ UIApplication.shared.delegate?.window??.rootViewController?.present(alert, animated: true, completion: completion)
+ }
+}
diff --git a/Example/Sources/Extensions/Settings+UserDefaults.swift b/Example/Sources/Extensions/Settings+UserDefaults.swift
index 37e25fac9..86e2c60bb 100644
--- a/Example/Sources/Extensions/Settings+UserDefaults.swift
+++ b/Example/Sources/Extensions/Settings+UserDefaults.swift
@@ -1,54 +1,51 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
extension UserDefaults {
-
- static let messagesKey = "mockMessages"
-
- // MARK: Mock Messages
-
- func setMockMessages(count: Int) {
- set(count, forKey: UserDefaults.messagesKey)
- synchronize()
- }
-
- func mockMessagesCount() -> Int {
- if let value = object(forKey: UserDefaults.messagesKey) as? Int {
- return value
- }
- return 20
+ static let messagesKey = "mockMessages"
+
+ static func isFirstLaunch() -> Bool {
+ let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag"
+ let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag)
+ if isFirstLaunch {
+ UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag)
+ UserDefaults.standard.synchronize()
}
-
- static func isFirstLaunch() -> Bool {
- let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag"
- let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag)
- if isFirstLaunch {
- UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag)
- UserDefaults.standard.synchronize()
- }
- return isFirstLaunch
+ return isFirstLaunch
+ }
+
+ // MARK: Mock Messages
+
+ func setMockMessages(count: Int) {
+ set(count, forKey: UserDefaults.messagesKey)
+ synchronize()
+ }
+
+ func mockMessagesCount() -> Int {
+ if let value = object(forKey: UserDefaults.messagesKey) as? Int {
+ return value
}
+ return 20
+ }
}
diff --git a/Example/Sources/Extensions/UIColor+Extensions.swift b/Example/Sources/Extensions/UIColor+Extensions.swift
index 8711dbbc0..42df1a929 100644
--- a/Example/Sources/Extensions/UIColor+Extensions.swift
+++ b/Example/Sources/Extensions/UIColor+Extensions.swift
@@ -1,29 +1,27 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
extension UIColor {
- static let primaryColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1)
+ static let primaryColor = UIColor(red: 69 / 255, green: 193 / 255, blue: 89 / 255, alpha: 1)
}
diff --git a/Example/Sources/Extensions/UIViewController+Extensions.swift b/Example/Sources/Extensions/UIViewController+Extensions.swift
index eff46fa30..d8f410c1c 100644
--- a/Example/Sources/Extensions/UIViewController+Extensions.swift
+++ b/Example/Sources/Extensions/UIViewController+Extensions.swift
@@ -1,67 +1,61 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
extension UIViewController {
-
- func updateTitleView(title: String, subtitle: String?, baseColor: UIColor = .white) {
-
- let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0))
- titleLabel.backgroundColor = UIColor.clear
- titleLabel.textColor = baseColor
- titleLabel.font = UIFont.systemFont(ofSize: 15)
- titleLabel.text = title
- titleLabel.textAlignment = .center
- titleLabel.adjustsFontSizeToFitWidth = true
- titleLabel.sizeToFit()
-
- let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0))
- subtitleLabel.textColor = baseColor.withAlphaComponent(0.95)
- subtitleLabel.font = UIFont.systemFont(ofSize: 12)
- subtitleLabel.text = subtitle
- subtitleLabel.textAlignment = .center
- subtitleLabel.adjustsFontSizeToFitWidth = true
- subtitleLabel.sizeToFit()
-
- let titleView = UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30))
- titleView.addSubview(titleLabel)
- if subtitle != nil {
- titleView.addSubview(subtitleLabel)
- } else {
- titleLabel.frame = titleView.frame
- }
- let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width
- if widthDiff < 0 {
- let newX = widthDiff / 2
- subtitleLabel.frame.origin.x = abs(newX)
- } else {
- let newX = widthDiff / 2
- titleLabel.frame.origin.x = newX
- }
-
- navigationItem.titleView = titleView
+ func updateTitleView(title: String, subtitle: String?) {
+ let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0))
+ titleLabel.backgroundColor = UIColor.clear
+ titleLabel.font = UIFont.systemFont(ofSize: 15)
+ titleLabel.text = title
+ titleLabel.textAlignment = .center
+ titleLabel.adjustsFontSizeToFitWidth = true
+ titleLabel.sizeToFit()
+
+ let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0))
+ subtitleLabel.font = UIFont.systemFont(ofSize: 12)
+ subtitleLabel.text = subtitle
+ subtitleLabel.textAlignment = .center
+ subtitleLabel.adjustsFontSizeToFitWidth = true
+ subtitleLabel.sizeToFit()
+
+ let titleView =
+ UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30))
+ titleView.addSubview(titleLabel)
+ if subtitle != nil {
+ titleView.addSubview(subtitleLabel)
+ } else {
+ titleLabel.frame = titleView.frame
}
-
+ let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width
+ if widthDiff < 0 {
+ let newX = widthDiff / 2
+ subtitleLabel.frame.origin.x = abs(newX)
+ } else {
+ let newX = widthDiff / 2
+ titleLabel.frame.origin.x = newX
+ }
+
+ navigationItem.titleView = titleView
+ }
}
diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift
index ecf406789..e2d7d4526 100644
--- a/Example/Sources/Layout/CustomMessageFlowLayout.swift
+++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift
@@ -1,68 +1,71 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-import UIKit
import MessageKit
+import UIKit
+
+// MARK: - CustomMessagesFlowLayout
open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout {
-
- open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self)
-
- open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
- if isSectionReservedForTypingIndicator(indexPath.section) {
- return typingIndicatorSizeCalculator
- }
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
- if case .custom = message.kind {
- return customMessageSizeCalculator
- }
- return super.cellSizeCalculatorForItem(at: indexPath)
+ open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self)
+
+ open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return typingIndicatorSizeCalculator
}
-
- open override func messageSizeCalculators() -> [MessageSizeCalculator] {
- var superCalculators = super.messageSizeCalculators()
- // Append any of your custom `MessageSizeCalculator` if you wish for the convenience
- // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...`
- superCalculators.append(customMessageSizeCalculator)
- return superCalculators
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+ if case .custom = message.kind {
+ return customMessageSizeCalculator
}
+ return super.cellSizeCalculatorForItem(at: indexPath)
+ }
+
+ open override func messageSizeCalculators() -> [MessageSizeCalculator] {
+ var superCalculators = super.messageSizeCalculators()
+ // Append any of your custom `MessageSizeCalculator` if you wish for the convenience
+ // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...`
+ superCalculators.append(customMessageSizeCalculator)
+ return superCalculators
+ }
}
+// MARK: - CustomMessageSizeCalculator
+
open class CustomMessageSizeCalculator: MessageSizeCalculator {
-
- public override init(layout: MessagesCollectionViewFlowLayout? = nil) {
- super.init()
- self.layout = layout
- }
-
- open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
- guard let layout = layout else { return .zero }
- let collectionViewWidth = layout.collectionView?.bounds.width ?? 0
- let contentInset = layout.collectionView?.contentInset ?? .zero
- let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right
- return CGSize(width: collectionViewWidth - inset, height: 44)
- }
-
+ // MARK: Lifecycle
+
+ public override init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+ self.layout = layout
+ }
+
+ // MARK: Open
+
+ open override func sizeForItem(at _: IndexPath) -> CGSize {
+ guard let layout = layout else { return .zero }
+ let collectionViewWidth = layout.collectionView?.bounds.width ?? 0
+ let contentInset = layout.collectionView?.contentInset ?? .zero
+ let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right
+ return CGSize(width: collectionViewWidth - inset, height: 44)
+ }
}
diff --git a/Example/Sources/Models/CustomLayoutSizeCalculator.swift b/Example/Sources/Models/CustomLayoutSizeCalculator.swift
new file mode 100644
index 000000000..06d417164
--- /dev/null
+++ b/Example/Sources/Models/CustomLayoutSizeCalculator.swift
@@ -0,0 +1,209 @@
+//
+// CustomLayoutSizeCalculator.swift
+// ChatExample
+//
+// Created by Vignesh J on 01/05/21.
+// Copyright © 2021 MessageKit. All rights reserved.
+//
+
+import MessageKit
+import UIKit
+
+class CustomLayoutSizeCalculator: CellSizeCalculator {
+ // MARK: Lifecycle
+
+ init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+
+ self.layout = layout
+ }
+
+ // MARK: Internal
+
+ var cellTopLabelVerticalPadding: CGFloat = 32
+ var cellTopLabelHorizontalPadding: CGFloat = 32
+ var cellMessageContainerHorizontalPadding: CGFloat = 48
+ var cellMessageContainerExtraSpacing: CGFloat = 16
+ var cellMessageContentVerticalPadding: CGFloat = 16
+ var cellMessageContentHorizontalPadding: CGFloat = 16
+ var cellDateLabelHorizontalPadding: CGFloat = 24
+ var cellDateLabelBottomPadding: CGFloat = 8
+
+ var messagesLayout: MessagesCollectionViewFlowLayout {
+ layout as! MessagesCollectionViewFlowLayout
+ }
+
+ var messageContainerMaxWidth: CGFloat {
+ messagesLayout.itemWidth -
+ cellMessageContainerHorizontalPadding -
+ cellMessageContainerExtraSpacing
+ }
+
+ var messagesDataSource: MessagesDataSource {
+ self.messagesLayout.messagesDataSource
+ }
+
+ override func sizeForItem(at indexPath: IndexPath) -> CGSize {
+ let dataSource = messagesDataSource
+ let message = dataSource.messageForItem(
+ at: indexPath,
+ in: messagesLayout.messagesCollectionView)
+ let itemHeight = cellContentHeight(
+ for: message,
+ at: indexPath)
+ return CGSize(
+ width: messagesLayout.itemWidth,
+ height: itemHeight)
+ }
+
+ func cellContentHeight(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGFloat
+ {
+ cellTopLabelSize(
+ for: message,
+ at: indexPath).height +
+ cellMessageBottomLabelSize(
+ for: message,
+ at: indexPath).height +
+ messageContainerSize(
+ for: message,
+ at: indexPath).height
+ }
+
+ // MARK: - Top cell Label
+
+ func cellTopLabelSize(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGSize
+ {
+ guard
+ let attributedText = messagesDataSource.cellTopLabelAttributedText(
+ for: message,
+ at: indexPath) else
+ {
+ return .zero
+ }
+
+ let maxWidth = messagesLayout.itemWidth - cellTopLabelHorizontalPadding
+ let size = attributedText.size(consideringWidth: maxWidth)
+ let height = size.height + cellTopLabelVerticalPadding
+
+ return CGSize(
+ width: maxWidth,
+ height: height)
+ }
+
+ func cellTopLabelFrame(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGRect
+ {
+ let size = cellTopLabelSize(
+ for: message,
+ at: indexPath)
+ guard size != .zero else {
+ return .zero
+ }
+
+ let origin = CGPoint(
+ x: cellTopLabelHorizontalPadding / 2,
+ y: 0)
+
+ return CGRect(
+ origin: origin,
+ size: size)
+ }
+
+ func cellMessageBottomLabelSize(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGSize
+ {
+ guard
+ let attributedText = messagesDataSource.messageBottomLabelAttributedText(
+ for: message,
+ at: indexPath) else
+ {
+ return .zero
+ }
+ let maxWidth = messageContainerMaxWidth - cellDateLabelHorizontalPadding
+
+ return attributedText.size(consideringWidth: maxWidth)
+ }
+
+ func cellMessageBottomLabelFrame(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGRect
+ {
+ let messageContainerSize = messageContainerSize(
+ for: message,
+ at: indexPath)
+ let labelSize = cellMessageBottomLabelSize(
+ for: message,
+ at: indexPath)
+ let x = messageContainerSize.width - labelSize.width - (cellDateLabelHorizontalPadding / 2)
+ let y = messageContainerSize.height - labelSize.height - cellDateLabelBottomPadding
+ let origin = CGPoint(
+ x: x,
+ y: y)
+
+ return CGRect(
+ origin: origin,
+ size: labelSize)
+ }
+
+ // MARK: - MessageContainer
+
+ func messageContainerSize(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGSize
+ {
+ let labelSize = cellMessageBottomLabelSize(
+ for: message,
+ at: indexPath)
+ let width = labelSize.width +
+ cellMessageContentHorizontalPadding +
+ cellDateLabelHorizontalPadding
+ let height = labelSize.height +
+ cellMessageContentVerticalPadding +
+ cellDateLabelBottomPadding
+
+ return CGSize(
+ width: width,
+ height: height)
+ }
+
+ func messageContainerFrame(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ fromCurrentSender: Bool)
+ -> CGRect
+ {
+ let y = cellTopLabelSize(
+ for: message,
+ at: indexPath).height
+ let size = messageContainerSize(
+ for: message,
+ at: indexPath)
+ let origin: CGPoint
+ if fromCurrentSender {
+ let x = messagesLayout.itemWidth -
+ size.width -
+ (cellMessageContainerHorizontalPadding / 2)
+ origin = CGPoint(x: x, y: y)
+ } else {
+ origin = CGPoint(
+ x: cellMessageContainerHorizontalPadding / 2,
+ y: y)
+ }
+
+ return CGRect(
+ origin: origin,
+ size: size)
+ }
+}
diff --git a/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift b/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift
new file mode 100644
index 000000000..32f04b288
--- /dev/null
+++ b/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift
@@ -0,0 +1,78 @@
+//
+// CustomTextMessageSizeCalculator.swift
+// ChatExample
+//
+// Created by Vignesh J on 30/04/21.
+// Copyright © 2021 MessageKit. All rights reserved.
+//
+
+import MessageKit
+import UIKit
+
+class CustomTextLayoutSizeCalculator: CustomLayoutSizeCalculator {
+ var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+ var cellMessageContainerRightSpacing: CGFloat = 16
+
+ override func messageContainerSize(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGSize
+ {
+ let size = super.messageContainerSize(
+ for: message,
+ at: indexPath)
+ let labelSize = messageLabelSize(
+ for: message,
+ at: indexPath)
+ let selfWidth = labelSize.width +
+ cellMessageContentHorizontalPadding +
+ cellMessageContainerRightSpacing
+ let width = max(selfWidth, size.width)
+ let height = size.height + labelSize.height
+
+ return CGSize(
+ width: width,
+ height: height)
+ }
+
+ func messageLabelSize(
+ for message: MessageType,
+ at _: IndexPath)
+ -> CGSize
+ {
+ let attributedText: NSAttributedString
+
+ let textMessageKind = message.kind
+ switch textMessageKind {
+ case .attributedText(let text):
+ attributedText = text
+ case .text(let text), .emoji(let text):
+ attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont])
+ default:
+ fatalError("messageLabelSize received unhandled MessageDataType: \(message.kind)")
+ }
+
+ let maxWidth = messageContainerMaxWidth -
+ cellMessageContentHorizontalPadding -
+ cellMessageContainerRightSpacing
+
+ return attributedText.size(consideringWidth: maxWidth)
+ }
+
+ func messageLabelFrame(
+ for message: MessageType,
+ at indexPath: IndexPath)
+ -> CGRect
+ {
+ let origin = CGPoint(
+ x: cellMessageContentHorizontalPadding / 2,
+ y: cellMessageContentVerticalPadding / 2)
+ let size = messageLabelSize(
+ for: message,
+ at: indexPath)
+
+ return CGRect(
+ origin: origin,
+ size: size)
+ }
+}
diff --git a/Example/Sources/Models/MockMessage.swift b/Example/Sources/Models/MockMessage.swift
index 12a365ba1..f47760936 100644
--- a/Example/Sources/Models/MockMessage.swift
+++ b/Example/Sources/Models/MockMessage.swift
@@ -1,170 +1,177 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import Foundation
-import UIKit
+import AVFoundation
import CoreLocation
+import Foundation
import MessageKit
-import AVFoundation
-
-private struct CoordinateItem: LocationItem {
+import UIKit
- var location: CLLocation
- var size: CGSize
+// MARK: - CoordinateItem
- init(location: CLLocation) {
- self.location = location
- self.size = CGSize(width: 240, height: 240)
- }
+private struct CoordinateItem: LocationItem {
+ var location: CLLocation
+ var size: CGSize
+ init(location: CLLocation) {
+ self.location = location
+ size = CGSize(width: 240, height: 240)
+ }
}
-private struct ImageMediaItem: MediaItem {
+// MARK: - ImageMediaItem
- var url: URL?
- var image: UIImage?
- var placeholderImage: UIImage
- var size: CGSize
-
- init(image: UIImage) {
- self.image = image
- self.size = CGSize(width: 240, height: 240)
- self.placeholderImage = UIImage()
- }
-
- init(imageURL: URL) {
- self.url = imageURL
- self.size = CGSize(width: 240, height: 240)
- self.placeholderImage = UIImage(imageLiteralResourceName: "image_message_placeholder")
- }
+private struct ImageMediaItem: MediaItem {
+ var url: URL?
+ var image: UIImage?
+ var placeholderImage: UIImage
+ var size: CGSize
+
+ init(image: UIImage) {
+ self.image = image
+ size = CGSize(width: 240, height: 240)
+ placeholderImage = UIImage()
+ }
+
+ init(imageURL: URL) {
+ url = imageURL
+ size = CGSize(width: 240, height: 240)
+ placeholderImage = UIImage(imageLiteralResourceName: "image_message_placeholder")
+ }
}
-private struct MockAudiotem: AudioItem {
-
- var url: URL
- var size: CGSize
- var duration: Float
+// MARK: - MockAudioItem
- init(url: URL) {
- self.url = url
- self.size = CGSize(width: 160, height: 35)
- // compute duration
- let audioAsset = AVURLAsset(url: url)
- self.duration = Float(CMTimeGetSeconds(audioAsset.duration))
- }
+private struct MockAudioItem: AudioItem {
+ var url: URL
+ var size: CGSize
+ var duration: Float
+ init(url: URL) {
+ self.url = url
+ size = CGSize(width: 160, height: 35)
+ // compute duration
+ let audioAsset = AVURLAsset(url: url)
+ duration = Float(CMTimeGetSeconds(audioAsset.duration))
+ }
}
+// MARK: - MockContactItem
+
struct MockContactItem: ContactItem {
-
- var displayName: String
- var initials: String
- var phoneNumbers: [String]
- var emails: [String]
-
- init(name: String, initials: String, phoneNumbers: [String] = [], emails: [String] = []) {
- self.displayName = name
- self.initials = initials
- self.phoneNumbers = phoneNumbers
- self.emails = emails
- }
-
+ var displayName: String
+ var initials: String
+ var phoneNumbers: [String]
+ var emails: [String]
+
+ init(name: String, initials: String, phoneNumbers: [String] = [], emails: [String] = []) {
+ displayName = name
+ self.initials = initials
+ self.phoneNumbers = phoneNumbers
+ self.emails = emails
+ }
}
+// MARK: - MockLinkItem
+
struct MockLinkItem: LinkItem {
- let text: String?
- let attributedText: NSAttributedString?
- let url: URL
- let title: String?
- let teaser: String
- let thumbnailImage: UIImage
+ let text: String?
+ let attributedText: NSAttributedString?
+ let url: URL
+ let title: String?
+ let teaser: String
+ let thumbnailImage: UIImage
}
-internal struct MockMessage: MessageType {
+// MARK: - MockMessage
- var messageId: String
- var sender: SenderType {
- return user
- }
- var sentDate: Date
- var kind: MessageKind
-
- var user: MockUser
-
- private init(kind: MessageKind, user: MockUser, messageId: String, date: Date) {
- self.kind = kind
- self.user = user
- self.messageId = messageId
- self.sentDate = date
- }
-
- init(custom: Any?, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .custom(custom), user: user, messageId: messageId, date: date)
- }
-
- init(text: String, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .text(text), user: user, messageId: messageId, date: date)
- }
-
- init(attributedText: NSAttributedString, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .attributedText(attributedText), user: user, messageId: messageId, date: date)
- }
-
- init(image: UIImage, user: MockUser, messageId: String, date: Date) {
- let mediaItem = ImageMediaItem(image: image)
- self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date)
- }
-
- init(imageURL: URL, user: MockUser, messageId: String, date: Date) {
- let mediaItem = ImageMediaItem(imageURL: imageURL)
- self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date)
- }
-
- init(thumbnail: UIImage, user: MockUser, messageId: String, date: Date) {
- let mediaItem = ImageMediaItem(image: thumbnail)
- self.init(kind: .video(mediaItem), user: user, messageId: messageId, date: date)
- }
-
- init(location: CLLocation, user: MockUser, messageId: String, date: Date) {
- let locationItem = CoordinateItem(location: location)
- self.init(kind: .location(locationItem), user: user, messageId: messageId, date: date)
- }
-
- init(emoji: String, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .emoji(emoji), user: user, messageId: messageId, date: date)
- }
-
- init(audioURL: URL, user: MockUser, messageId: String, date: Date) {
- let audioItem = MockAudiotem(url: audioURL)
- self.init(kind: .audio(audioItem), user: user, messageId: messageId, date: date)
- }
-
- init(contact: MockContactItem, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .contact(contact), user: user, messageId: messageId, date: date)
- }
-
- init(linkItem: LinkItem, user: MockUser, messageId: String, date: Date) {
- self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId, date: date)
- }
+internal struct MockMessage: MessageType {
+ // MARK: Lifecycle
+
+ private init(kind: MessageKind, user: MockUser, messageId: String, date: Date) {
+ self.kind = kind
+ self.user = user
+ self.messageId = messageId
+ sentDate = date
+ }
+
+ init(custom: Any?, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .custom(custom), user: user, messageId: messageId, date: date)
+ }
+
+ init(text: String, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .text(text), user: user, messageId: messageId, date: date)
+ }
+
+ init(attributedText: NSAttributedString, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .attributedText(attributedText), user: user, messageId: messageId, date: date)
+ }
+
+ init(image: UIImage, user: MockUser, messageId: String, date: Date) {
+ let mediaItem = ImageMediaItem(image: image)
+ self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date)
+ }
+
+ init(imageURL: URL, user: MockUser, messageId: String, date: Date) {
+ let mediaItem = ImageMediaItem(imageURL: imageURL)
+ self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date)
+ }
+
+ init(thumbnail: UIImage, user: MockUser, messageId: String, date: Date) {
+ let mediaItem = ImageMediaItem(image: thumbnail)
+ self.init(kind: .video(mediaItem), user: user, messageId: messageId, date: date)
+ }
+
+ init(location: CLLocation, user: MockUser, messageId: String, date: Date) {
+ let locationItem = CoordinateItem(location: location)
+ self.init(kind: .location(locationItem), user: user, messageId: messageId, date: date)
+ }
+
+ init(emoji: String, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .emoji(emoji), user: user, messageId: messageId, date: date)
+ }
+
+ init(audioURL: URL, user: MockUser, messageId: String, date: Date) {
+ let audioItem = MockAudioItem(url: audioURL)
+ self.init(kind: .audio(audioItem), user: user, messageId: messageId, date: date)
+ }
+
+ init(contact: MockContactItem, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .contact(contact), user: user, messageId: messageId, date: date)
+ }
+
+ init(linkItem: LinkItem, user: MockUser, messageId: String, date: Date) {
+ self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId, date: date)
+ }
+
+ // MARK: Internal
+
+ var messageId: String
+ var sentDate: Date
+ var kind: MessageKind
+
+ var user: MockUser
+
+ var sender: SenderType {
+ user
+ }
}
diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift
index bdf1b63f3..41c9f1b11 100644
--- a/Example/Sources/Models/MockSocket.swift
+++ b/Example/Sources/Models/MockSocket.swift
@@ -1,85 +1,94 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MessageKit
+import UIKit
+@MainActor
final class MockSocket {
-
- static var shared = MockSocket()
-
- private var timer: Timer?
-
- private var queuedMessage: MockMessage?
-
- private var onNewMessageCode: ((MockMessage) -> Void)?
-
- private var onTypingStatusCode: (() -> Void)?
-
- private var connectedUsers: [MockUser] = []
-
- private init() {}
-
- @discardableResult
- func connect(with senders: [MockUser]) -> Self {
- disconnect()
- connectedUsers = senders
- timer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true)
- return self
- }
-
- @discardableResult
- func disconnect() -> Self {
- timer?.invalidate()
- timer = nil
- onTypingStatusCode = nil
- onNewMessageCode = nil
- return self
- }
-
- @discardableResult
- func onNewMessage(code: @escaping (MockMessage) -> Void) -> Self {
- onNewMessageCode = code
- return self
- }
-
- @discardableResult
- func onTypingStatus(code: @escaping () -> Void) -> Self {
- onTypingStatusCode = code
- return self
- }
-
- @objc
- private func handleTimer() {
- if let message = queuedMessage {
- onNewMessageCode?(message)
- queuedMessage = nil
- } else {
- let sender = connectedUsers.random()!
- let message = SampleData.shared.randomMessage(allowedSenders: [sender])
- queuedMessage = message
- onTypingStatusCode?()
- }
+ // MARK: Lifecycle
+
+ private init() { }
+
+ // MARK: Internal
+
+ static var shared = MockSocket()
+
+ @discardableResult
+ func connect(with senders: [MockUser]) -> Self {
+ disconnect()
+ connectedUsers = senders
+ timer = Timer.scheduledTimer(
+ timeInterval: 2.5,
+ target: self,
+ selector: #selector(handleTimer),
+ userInfo: nil,
+ repeats: true)
+ return self
+ }
+
+ @discardableResult
+ func disconnect() -> Self {
+ timer?.invalidate()
+ timer = nil
+ onTypingStatusCode = nil
+ onNewMessageCode = nil
+ return self
+ }
+
+ @discardableResult
+ func onNewMessage(code: @escaping (MockMessage) -> Void) -> Self {
+ onNewMessageCode = code
+ return self
+ }
+
+ @discardableResult
+ func onTypingStatus(code: @escaping () -> Void) -> Self {
+ onTypingStatusCode = code
+ return self
+ }
+
+ // MARK: Private
+
+ private var timer: Timer?
+
+ private var queuedMessage: MockMessage?
+
+ private var onNewMessageCode: ((MockMessage) -> Void)?
+
+ private var onTypingStatusCode: (() -> Void)?
+
+ private var connectedUsers: [MockUser] = []
+
+ @objc
+ private func handleTimer() {
+ if let message = queuedMessage {
+ onNewMessageCode?(message)
+ queuedMessage = nil
+ } else {
+ let sender = connectedUsers.random()!
+ let message = SampleData.shared.randomMessage(allowedSenders: [sender])
+ queuedMessage = message
+ onTypingStatusCode?()
}
+ }
}
diff --git a/Example/Sources/Models/MockUser.swift b/Example/Sources/Models/MockUser.swift
index e4f2362fd..4274b2c2e 100644
--- a/Example/Sources/Models/MockUser.swift
+++ b/Example/Sources/Models/MockUser.swift
@@ -1,31 +1,29 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import MessageKit
struct MockUser: SenderType, Equatable {
- var senderId: String
- var displayName: String
+ var senderId: String
+ var displayName: String
}
diff --git a/Example/Sources/Resources/Assets.xcassets/Contents.json b/Example/Sources/Resources/Assets.xcassets/Contents.json
index da4a164c9..73c00596a 100644
--- a/Example/Sources/Resources/Assets.xcassets/Contents.json
+++ b/Example/Sources/Resources/Assets.xcassets/Contents.json
@@ -1,6 +1,6 @@
{
"info" : {
- "version" : 1,
- "author" : "xcode"
+ "author" : "xcode",
+ "version" : 1
}
-}
\ No newline at end of file
+}
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..be5d8ca35
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..11eb26a18
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..1bb9c5579
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/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift
index 9e79232a2..d575e64cc 100644
--- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift
+++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift
@@ -1,441 +1,530 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
-import MapKit
-import MessageKit
import InputBarAccessoryView
import Kingfisher
+import MapKit
+import MessageKit
+import UIKit
-final class AdvancedExampleViewController: ChatViewController {
-
- let outgoingAvatarOverlap: CGFloat = 17.5
-
- override func viewDidLoad() {
- messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout())
- messagesCollectionView.register(CustomCell.self)
- super.viewDidLoad()
-
- updateTitleView(title: "MessageKit", subtitle: "2 Online")
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
- .onTypingStatus { [weak self] in
- self?.setTypingIndicatorViewHidden(false)
- }.onNewMessage { [weak self] message in
- self?.setTypingIndicatorViewHidden(true, performUpdates: {
- self?.insertMessage(message)
- })
- }
- }
-
- override func loadFirstMessages() {
- DispatchQueue.global(qos: .userInitiated).async {
- let count = UserDefaults.standard.mockMessagesCount()
- SampleData.shared.getAdvancedMessages(count: count) { messages in
- DispatchQueue.main.async {
- self.messageList = messages
- self.messagesCollectionView.reloadData()
- self.messagesCollectionView.scrollToLastItem()
- }
- }
- }
- }
-
- override func loadMoreMessages() {
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
- SampleData.shared.getAdvancedMessages(count: 20) { messages in
- DispatchQueue.main.async {
- self.messageList.insert(contentsOf: messages, at: 0)
- self.messagesCollectionView.reloadDataAndKeepOffset()
- self.refreshControl.endRefreshing()
- }
- }
- }
- }
-
- override func configureMessageCollectionView() {
- super.configureMessageCollectionView()
-
- let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
- layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
-
- // Hide the outgoing avatar and adjust the label alignment to line up with the messages
- layout?.setMessageOutgoingAvatarSize(.zero)
- layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
- layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
-
- // Set outgoing avatar to overlap with the message bubble
- layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0)))
- layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
- layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18))
-
- layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
- layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0))
- layout?.setMessageIncomingAccessoryViewPosition(.messageBottom)
- layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
- layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))
-
- messagesCollectionView.messagesLayoutDelegate = self
- messagesCollectionView.messagesDisplayDelegate = self
- }
-
- override func configureMessageInputBar() {
- super.configureMessageInputBar()
-
- messageInputBar.isTranslucent = true
- messageInputBar.separatorLine.isHidden = true
- messageInputBar.inputTextView.tintColor = .primaryColor
- messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)
- messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
- messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36)
- messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36)
- messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor
- messageInputBar.inputTextView.layer.borderWidth = 1.0
- messageInputBar.inputTextView.layer.cornerRadius = 16.0
- messageInputBar.inputTextView.layer.masksToBounds = true
- messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
- configureInputBarItems()
- }
+// MARK: - AdvancedExampleViewController
- private func configureInputBarItems() {
- messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false)
- messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
- messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
- messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false)
- messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up")
- messageInputBar.sendButton.title = nil
- messageInputBar.sendButton.imageView?.layer.cornerRadius = 16
- let charCountButton = InputBarButtonItem()
- .configure {
- $0.title = "0/140"
- $0.contentHorizontalAlignment = .right
- $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal)
- $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold)
- $0.setSize(CGSize(width: 50, height: 25), animated: false)
- }.onTextViewDidChange { (item, textView) in
- item.title = "\(textView.text.count)/140"
- let isOverLimit = textView.text.count > 140
- item.inputBarAccessoryView?.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit
- if isOverLimit {
- item.inputBarAccessoryView?.sendButton.isEnabled = false
- }
- let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1)
- item.setTitleColor(color, for: .normal)
- }
- let bottomItems = [.flexibleSpace, charCountButton]
-
- configureInputBarPadding()
-
- messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)
-
- // This just adds some more flare
- messageInputBar.sendButton
- .onEnabled { item in
- UIView.animate(withDuration: 0.3, animations: {
- item.imageView?.backgroundColor = .primaryColor
- })
- }.onDisabled { item in
- UIView.animate(withDuration: 0.3, animations: {
- item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
- })
- }
- }
-
- /// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if neccesary
- /// See the InputBar diagram here to visualize how each of these would take effect:
- /// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png
- private func configureInputBarPadding() {
-
- // Entire InputBar padding
- messageInputBar.padding.bottom = 8
-
- // or MiddleContentView padding
- messageInputBar.middleContentViewPadding.right = -38
-
- // or InputTextView padding
- messageInputBar.inputTextView.textContainerInset.bottom = 8
-
- }
-
- // MARK: - Helpers
-
- func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
- return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
- }
-
- func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
- guard indexPath.section - 1 >= 0 else { return false }
- return messageList[indexPath.section].user == messageList[indexPath.section - 1].user
- }
-
- func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
- guard indexPath.section + 1 < messageList.count else { return false }
- return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
- }
-
- func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) {
- updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...")
- setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in
- if success, self?.isLastSectionVisible() == true {
- self?.messagesCollectionView.scrollToLastItem(animated: true)
- }
- }
- }
-
- private func makeButton(named: String) -> InputBarButtonItem {
- return InputBarButtonItem()
- .configure {
- $0.spacing = .fixed(10)
- $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
- $0.setSize(CGSize(width: 25, height: 25), animated: false)
- $0.tintColor = UIColor(white: 0.8, alpha: 1)
- }.onSelected {
- $0.tintColor = .primaryColor
- }.onDeselected {
- $0.tintColor = UIColor(white: 0.8, alpha: 1)
- }.onTouchUpInside {
- print("Item Tapped")
- let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
- let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- actionSheet.addAction(action)
- if let popoverPresentationController = actionSheet.popoverPresentationController {
- popoverPresentationController.sourceView = $0
- popoverPresentationController.sourceRect = $0.frame
- }
- self.navigationController?.present(actionSheet, animated: true, completion: nil)
+final class AdvancedExampleViewController: ChatViewController {
+ // MARK: Public
+
+ // MARK: - UICollectionViewDataSource
+
+ public override func collectionView(
+ _ collectionView: UICollectionView,
+ cellForItemAt indexPath: IndexPath)
+ -> UICollectionViewCell
+ {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError("Ouch. nil data source for messages")
+ }
+
+ // Very important to check this when overriding `cellForItemAt`
+ // Super method will handle returning the typing indicator cell
+ guard !isSectionReservedForTypingIndicator(indexPath.section) else {
+ return super.collectionView(collectionView, cellForItemAt: indexPath)
+ }
+
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+ if case .custom = message.kind {
+ let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ return super.collectionView(collectionView, cellForItemAt: indexPath)
+ }
+
+ // MARK: Internal
+
+ let outgoingAvatarOverlap: CGFloat = 17.5
+
+ override func viewDidLoad() {
+ messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout())
+ messagesCollectionView.register(CustomCell.self)
+ super.viewDidLoad()
+
+ updateTitleView(title: "MessageKit", subtitle: "2 Online")
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
+ .onTypingStatus { [weak self] in
+ self?.setTypingIndicatorViewHidden(false, animated: true)
+ }.onNewMessage { [weak self] message in
+ self?.setTypingIndicatorViewHidden(true, animated: false, performUpdates: {
+ self?.insertMessage(message)
+ })
+ }
+ }
+
+ override func loadFirstMessages() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ let count = UserDefaults.standard.mockMessagesCount()
+ SampleData.shared.getAdvancedMessages(count: count) { messages in
+ DispatchQueue.main.async {
+ self.messageList = messages
+ self.messagesCollectionView.reloadData()
+ self.messagesCollectionView.scrollToLastItem()
}
+ }
}
-
- // MARK: - UICollectionViewDataSource
-
- public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
-
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError("Ouch. nil data source for messages")
- }
+ }
- // Very important to check this when overriding `cellForItemAt`
- // Super method will handle returning the typing indicator cell
- guard !isSectionReservedForTypingIndicator(indexPath.section) else {
- return super.collectionView(collectionView, cellForItemAt: indexPath)
+ override func loadMoreMessages() {
+ DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
+ SampleData.shared.getAdvancedMessages(count: 20) { messages in
+ DispatchQueue.main.async {
+ self.messageList.insert(contentsOf: messages, at: 0)
+ self.messagesCollectionView.reloadDataAndKeepOffset()
+ self.refreshControl.endRefreshing()
}
-
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
- if case .custom = message.kind {
- let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- }
- return super.collectionView(collectionView, cellForItemAt: indexPath)
- }
-
- // MARK: - MessagesDataSource
-
- override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- if isTimeLabelVisible(at: indexPath) {
- return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray])
+ }
+ }
+ }
+
+ override func configureMessageCollectionView() {
+ super.configureMessageCollectionView()
+
+ let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
+ layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
+
+ // Hide the outgoing avatar and adjust the label alignment to line up with the messages
+ layout?.setMessageOutgoingAvatarSize(.zero)
+ layout?
+ .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(
+ textAlignment: .right,
+ textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
+ layout?
+ .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(
+ textAlignment: .right,
+ textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
+
+ // Set outgoing avatar to overlap with the message bubble
+ layout?
+ .setMessageIncomingMessageTopLabelAlignment(LabelAlignment(
+ textAlignment: .left,
+ textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0)))
+ layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
+ layout?
+ .setMessageIncomingMessagePadding(UIEdgeInsets(
+ top: -outgoingAvatarOverlap,
+ left: -18,
+ bottom: outgoingAvatarOverlap,
+ right: 18))
+
+ layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
+ layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0))
+ layout?.setMessageIncomingAccessoryViewPosition(.messageBottom)
+ layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
+ layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))
+
+ messagesCollectionView.messagesLayoutDelegate = self
+ messagesCollectionView.messagesDisplayDelegate = self
+ }
+
+ override func configureMessageInputBar() {
+ // super.configureMessageInputBar()
+
+ messageInputBar = CameraInputBarAccessoryView()
+ messageInputBar.delegate = self
+ messageInputBar.inputTextView.tintColor = .primaryColor
+ messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal)
+ messageInputBar.sendButton.setTitleColor(
+ UIColor.primaryColor.withAlphaComponent(0.3),
+ for: .highlighted)
+
+ messageInputBar.isTranslucent = true
+ messageInputBar.separatorLine.isHidden = true
+ messageInputBar.inputTextView.tintColor = .primaryColor
+ messageInputBar.inputTextView.backgroundColor = UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1)
+ messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
+ messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36)
+ messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36)
+ messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1).cgColor
+ messageInputBar.inputTextView.layer.borderWidth = 1.0
+ messageInputBar.inputTextView.layer.cornerRadius = 16.0
+ messageInputBar.inputTextView.layer.masksToBounds = true
+ messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
+ configureInputBarItems()
+ inputBarType = .custom(messageInputBar)
+ }
+
+ // MARK: - Helpers
+
+ func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
+ indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
+ }
+
+ func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
+ guard indexPath.section - 1 >= 0 else { return false }
+ return messageList[indexPath.section].user == messageList[indexPath.section - 1].user
+ }
+
+ func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
+ guard indexPath.section + 1 < messageList.count else { return false }
+ return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
+ }
+
+ func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, performUpdates updates: (() -> Void)? = nil) {
+ updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...")
+ setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates) { [weak self] success in
+ if success, self?.isLastSectionVisible() == true {
+ self?.messagesCollectionView.scrollToLastItem(animated: true)
+ }
+ }
+ }
+
+ // MARK: - MessagesDataSource
+
+ override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if isTimeLabelVisible(at: indexPath) {
+ return NSAttributedString(
+ string: MessageKitDateFormatter.shared.string(from: message.sentDate),
+ attributes: [
+ NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
+ NSAttributedString.Key.foregroundColor: UIColor.darkGray,
+ ])
+ }
+ return nil
+ }
+
+ override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if !isPreviousMessageSameSender(at: indexPath) {
+ let name = message.sender.displayName
+ return NSAttributedString(
+ string: name,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+ return nil
+ }
+
+ override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if !isNextMessageSameSender(at: indexPath), isFromCurrentSender(message: message) {
+ return NSAttributedString(
+ string: "Delivered",
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+ return nil
+ }
+
+ // MARK: Private
+
+ private func configureInputBarItems() {
+ messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false)
+ messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
+ messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
+ messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false)
+ messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up")
+ messageInputBar.sendButton.title = nil
+ messageInputBar.sendButton.imageView?.layer.cornerRadius = 16
+ let charCountButton = InputBarButtonItem()
+ .configure {
+ $0.title = "0/140"
+ $0.contentHorizontalAlignment = .right
+ $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal)
+ $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold)
+ $0.setSize(CGSize(width: 50, height: 25), animated: false)
+ }.onTextViewDidChange { item, textView in
+ item.title = "\(textView.text.count)/140"
+ let isOverLimit = textView.text.count > 140
+ item.inputBarAccessoryView?
+ .shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit
+ if isOverLimit {
+ item.inputBarAccessoryView?.sendButton.isEnabled = false
}
- return nil
- }
-
- override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- if !isPreviousMessageSameSender(at: indexPath) {
- let name = message.sender.displayName
- return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1)
+ item.setTitleColor(color, for: .normal)
+ }
+ let bottomItems = [.flexibleSpace, charCountButton]
+
+ configureInputBarPadding()
+
+ messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)
+
+ // This just adds some more flare
+ messageInputBar.sendButton
+ .onEnabled { item in
+ UIView.animate(withDuration: 0.3, animations: {
+ item.imageView?.backgroundColor = .primaryColor
+ })
+ }.onDisabled { item in
+ UIView.animate(withDuration: 0.3, animations: {
+ item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
+ })
+ }
+ }
+
+ /// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if necessary
+ /// See the InputBar diagram here to visualize how each of these would take effect:
+ /// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png
+ private func configureInputBarPadding() {
+ // Entire InputBar padding
+ messageInputBar.padding.bottom = 8
+
+ // or MiddleContentView padding
+ messageInputBar.middleContentViewPadding.right = -38
+
+ // or InputTextView padding
+ messageInputBar.inputTextView.textContainerInset.bottom = 8
+ }
+
+ private func makeButton(named: String) -> InputBarButtonItem {
+ InputBarButtonItem()
+ .configure {
+ $0.spacing = .fixed(10)
+ $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
+ $0.setSize(CGSize(width: 25, height: 25), animated: false)
+ $0.tintColor = UIColor(white: 0.8, alpha: 1)
+ }.onSelected {
+ $0.tintColor = .primaryColor
+ }.onDeselected {
+ $0.tintColor = UIColor(white: 0.8, alpha: 1)
+ }.onTouchUpInside {
+ print("Item Tapped")
+ let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+ let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
+ actionSheet.addAction(action)
+ if let popoverPresentationController = actionSheet.popoverPresentationController {
+ popoverPresentationController.sourceView = $0
+ popoverPresentationController.sourceRect = $0.frame
}
- return nil
- }
-
- override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
-
- if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) {
- return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
- return nil
- }
+ self.navigationController?.present(actionSheet, animated: true, completion: nil)
+ }
+ }
}
-// MARK: - MessagesDisplayDelegate
+// MARK: MessagesDisplayDelegate
extension AdvancedExampleViewController: MessagesDisplayDelegate {
+ // MARK: - Text Messages
+
+ func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .white : .darkText
+ }
+
+ func detectorAttributes(
+ for detector: DetectorType,
+ and message: MessageType,
+ at _: IndexPath) -> [NSAttributedString.Key: Any]
+ {
+ switch detector {
+ case .hashtag, .mention:
+ if isFromCurrentSender(message: message) {
+ return [.foregroundColor: UIColor.white]
+ } else {
+ return [.foregroundColor: UIColor.primaryColor]
+ }
+ default: return MessageLabel.defaultAttributes
+ }
+ }
+
+ func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
+ [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
+ }
+
+ // MARK: - All Messages
+
+ func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
+ }
+
+ func messageStyle(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
+ var corners: UIRectCorner = []
+
+ if isFromCurrentSender(message: message) {
+ corners.formUnion(.topLeft)
+ corners.formUnion(.bottomLeft)
+ if !isPreviousMessageSameSender(at: indexPath) {
+ corners.formUnion(.topRight)
+ }
+ if !isNextMessageSameSender(at: indexPath) {
+ corners.formUnion(.bottomRight)
+ }
+ } else {
+ corners.formUnion(.topRight)
+ corners.formUnion(.bottomRight)
+ if !isPreviousMessageSameSender(at: indexPath) {
+ corners.formUnion(.topLeft)
+ }
+ if !isNextMessageSameSender(at: indexPath) {
+ corners.formUnion(.bottomLeft)
+ }
+ }
+
+ return .custom { view in
+ let radius: CGFloat = 16
+ let path = UIBezierPath(
+ roundedRect: view.bounds,
+ byRoundingCorners: corners,
+ cornerRadii: CGSize(width: radius, height: radius))
+ let mask = CAShapeLayer()
+ mask.path = path.cgPath
+ view.layer.mask = mask
+ }
+ }
+
+ func configureAvatarView(
+ _ avatarView: AvatarView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in _: MessagesCollectionView)
+ {
+ let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
+ avatarView.set(avatar: avatar)
+ avatarView.isHidden = isNextMessageSameSender(at: indexPath)
+ avatarView.layer.borderWidth = 2
+ avatarView.layer.borderColor = UIColor.primaryColor.cgColor
+ }
+
+ func configureAccessoryView(_ accessoryView: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ // Cells are reused, so only add a button here once. For real use you would need to
+ // ensure any subviews are removed if not needed
+ accessoryView.subviews.forEach { $0.removeFromSuperview() }
+ accessoryView.backgroundColor = .clear
+
+ let shouldShow = Int.random(in: 0 ... 10) == 0
+ guard shouldShow else { return }
+
+ let button = UIButton(type: .infoLight)
+ button.tintColor = .primaryColor
+ accessoryView.addSubview(button)
+ button.frame = accessoryView.bounds
+ button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
+ accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
+ accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
+ }
+
+ func configureMediaMessageImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ {
+ if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
+ imageView.kf.setImage(with: imageURL)
+ } else {
+ imageView.kf.cancelDownloadTask()
+ }
+ }
+
+ // MARK: - Location Messages
+
+ func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? {
+ let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
+ let pinImage = #imageLiteral(resourceName: "ic_map_marker")
+ annotationView.image = pinImage
+ annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
+ return annotationView
+ }
+
+ func animationBlockForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
+ {
+ { view in
+ view.layer.transform = CATransform3DMakeScale(2, 2, 2)
+ UIView.animate(
+ withDuration: 0.6,
+ delay: 0,
+ usingSpringWithDamping: 0.9,
+ initialSpringVelocity: 0,
+ options: [],
+ animations: {
+ view.layer.transform = CATransform3DIdentity
+ },
+ completion: nil)
+ }
+ }
+
+ func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions(
+ showsBuildings: true,
+ showsPointsOfInterest: true,
+ span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
+ }
+
+ // MARK: - Audio Messages
+
+ func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .white : .primaryColor
+ }
+
+ func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
+ audioController
+ .configureAudioCell(
+ cell,
+ message: message) // this is needed especially when the cell is reconfigure while is playing sound
+ }
+}
- // MARK: - Text Messages
-
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .white : .darkText
- }
-
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
- switch detector {
- case .hashtag, .mention:
- if isFromCurrentSender(message: message) {
- return [.foregroundColor: UIColor.white]
- } else {
- return [.foregroundColor: UIColor.primaryColor]
- }
- default: return MessageLabel.defaultAttributes
- }
- }
-
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
- return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
- }
-
- // MARK: - All Messages
-
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
- }
-
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
-
- var corners: UIRectCorner = []
-
- if isFromCurrentSender(message: message) {
- corners.formUnion(.topLeft)
- corners.formUnion(.bottomLeft)
- if !isPreviousMessageSameSender(at: indexPath) {
- corners.formUnion(.topRight)
- }
- if !isNextMessageSameSender(at: indexPath) {
- corners.formUnion(.bottomRight)
- }
- } else {
- corners.formUnion(.topRight)
- corners.formUnion(.bottomRight)
- if !isPreviousMessageSameSender(at: indexPath) {
- corners.formUnion(.topLeft)
- }
- if !isNextMessageSameSender(at: indexPath) {
- corners.formUnion(.bottomLeft)
- }
- }
-
- return .custom { view in
- let radius: CGFloat = 16
- let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
- let mask = CAShapeLayer()
- mask.path = path.cgPath
- view.layer.mask = mask
- }
- }
-
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
- avatarView.set(avatar: avatar)
- avatarView.isHidden = isNextMessageSameSender(at: indexPath)
- avatarView.layer.borderWidth = 2
- avatarView.layer.borderColor = UIColor.primaryColor.cgColor
- }
-
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- // Cells are reused, so only add a button here once. For real use you would need to
- // ensure any subviews are removed if not needed
- accessoryView.subviews.forEach { $0.removeFromSuperview() }
- accessoryView.backgroundColor = .clear
-
- let shouldShow = Int.random(in: 0...10) == 0
- guard shouldShow else { return }
-
- let button = UIButton(type: .infoLight)
- button.tintColor = .primaryColor
- accessoryView.addSubview(button)
- button.frame = accessoryView.bounds
- button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
- accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
- accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
- }
+// MARK: MessagesLayoutDelegate
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
- imageView.kf.setImage(with: imageURL)
- } else {
- imageView.kf.cancelDownloadTask()
- }
- }
-
- // MARK: - Location Messages
-
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
- let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
- let pinImage = #imageLiteral(resourceName: "ic_map_marker")
- annotationView.image = pinImage
- annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
- return annotationView
- }
-
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
- return { view in
- view.layer.transform = CATransform3DMakeScale(2, 2, 2)
- UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
- view.layer.transform = CATransform3DIdentity
- }, completion: nil)
- }
- }
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
-
- return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
+extension AdvancedExampleViewController: MessagesLayoutDelegate {
+ func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ if isTimeLabelVisible(at: indexPath) {
+ return 18
}
+ return 0
+ }
- // MARK: - Audio Messages
-
- func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return self.isFromCurrentSender(message: message) ? .white : .primaryColor
+ func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ if isFromCurrentSender(message: message) {
+ return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
+ } else {
+ return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0
}
+ }
- func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
- audioController.configureAudioCell(cell, message: message) // this is needed especily when the cell is reconfigure while is playing sound
- }
-
+ func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
+ }
}
-// MARK: - MessagesLayoutDelegate
-
-extension AdvancedExampleViewController: MessagesLayoutDelegate {
-
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- if isTimeLabelVisible(at: indexPath) {
- return 18
- }
- return 0
- }
-
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- if isFromCurrentSender(message: message) {
- return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
- } else {
- return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0
- }
- }
+// MARK: CameraInputBarAccessoryViewDelegate
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
+extension AdvancedExampleViewController: CameraInputBarAccessoryViewDelegate {
+ func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) {
+ for item in attachments {
+ if case .image(let image) = item {
+ self.sendImageMessage(photo: image)
+ }
}
+ inputBar.invalidatePlugins()
+ }
+ func sendImageMessage(photo: UIImage) {
+ let photoMessage = MockMessage(image: photo, user: currentSender as! MockUser, messageId: UUID().uuidString, date: Date())
+ insertMessage(photoMessage)
+ }
}
diff --git a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift
index 63ce2acfa..c5b2de5da 100644
--- a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift
+++ b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift
@@ -1,389 +1,432 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
-import MessageKit
import InputBarAccessoryView
import Kingfisher
+import MessageKit
+import UIKit
-final class AutocompleteExampleViewController: ChatViewController {
-
- lazy var joinChatButton: UIButton = {
- let button = UIButton()
- button.layer.cornerRadius = 16
- button.backgroundColor = .primaryColor
- button.setTitle("JOIN CHAT", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted)
- button.addTarget(self, action: #selector(joinChat), for: .touchUpInside)
- return button
- }()
-
- /// The object that manages autocomplete, from InputBarAccessoryView
- lazy var autocompleteManager: AutocompleteManager = { [unowned self] in
- let manager = AutocompleteManager(for: self.messageInputBar.inputTextView)
- manager.delegate = self
- manager.dataSource = self
- return manager
- }()
-
- var hastagAutocompletes: [AutocompleteCompletion] = {
- var array: [AutocompleteCompletion] = []
- for _ in 1...100 {
- array.append(AutocompleteCompletion(text: Lorem.word(), context: nil))
- }
- return array
- }()
-
- // Completions loaded async that get appeneded to local cached completions
- var asyncCompletions: [AutocompleteCompletion] = []
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
- .onTypingStatus { [weak self] in
- self?.setTypingIndicatorViewHidden(false)
- }.onNewMessage { [weak self] message in
- self?.setTypingIndicatorViewHidden(true, performUpdates: {
- self?.insertMessage(message)
- })
- }
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- messageInputBar.inputTextView.keyboardType = .twitter
-
- // Configure AutocompleteManager
- autocompleteManager.register(prefix: "@", with: [.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.primaryColor, .backgroundColor: UIColor.primaryColor.withAlphaComponent(0.3)])
- autocompleteManager.register(prefix: "#")
- autocompleteManager.maxSpaceCountDuringCompletion = 1 // Allow for autocompletes with a space
-
- // Set plugins
- messageInputBar.inputPlugins = [autocompleteManager]
- }
-
- override func configureMessageCollectionView() {
- super.configureMessageCollectionView()
-
- let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
- layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
- layout?.setMessageOutgoingCellBottomLabelAlignment(.init(textAlignment: .right, textInsets: .zero))
- layout?.setMessageOutgoingAvatarSize(.zero)
- layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12)))
- layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12)))
-
- messagesCollectionView.messagesLayoutDelegate = self
- messagesCollectionView.messagesDisplayDelegate = self
-
- additionalBottomInset = 30
- }
-
- override func configureMessageInputBar() {
- super.configureMessageInputBar()
- messageInputBar.layer.shadowColor = UIColor.black.cgColor
- messageInputBar.layer.shadowRadius = 4
- messageInputBar.layer.shadowOpacity = 0.3
- messageInputBar.layer.shadowOffset = CGSize(width: 0, height: 0)
- messageInputBar.separatorLine.isHidden = true
- messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
- messageInputBar.setMiddleContentView(joinChatButton, animated: false)
- }
-
- private func configureMessageInputBarForChat() {
- messageInputBar.setMiddleContentView(messageInputBar.inputTextView, animated: false)
- messageInputBar.setRightStackViewWidthConstant(to: 52, animated: false)
- let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), .flexibleSpace]
- messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)
-
- messageInputBar.sendButton.activityViewColor = .white
- messageInputBar.sendButton.backgroundColor = .primaryColor
- messageInputBar.sendButton.layer.cornerRadius = 10
- messageInputBar.sendButton.setTitleColor(.white, for: .normal)
- messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted)
- messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .disabled)
- messageInputBar.sendButton
- .onSelected { item in
- item.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
- }.onDeselected { item in
- item.transform = .identity
- }
- }
-
- @objc
- func joinChat() {
- configureMessageInputBarForChat()
- }
-
- // MARK: - Helpers
-
- func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
- return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
- }
-
- func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
- guard indexPath.section - 1 >= 0 else { return false }
- return messageList[indexPath.section].user == messageList[indexPath.section - 1].user
- }
-
- func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
- guard indexPath.section + 1 < messageList.count else { return false }
- return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
- }
-
- func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) {
- setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in
- if success, self?.isLastSectionVisible() == true {
- self?.messagesCollectionView.scrollToLastItem(animated: true)
- }
- }
- }
-
- private func makeButton(named: String) -> InputBarButtonItem {
- return InputBarButtonItem()
- .configure {
- $0.spacing = .fixed(10)
- $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
- $0.setSize(CGSize(width: 25, height: 25), animated: false)
- $0.tintColor = UIColor(white: 0.8, alpha: 1)
- }.onSelected {
- $0.tintColor = .primaryColor
- }.onDeselected {
- $0.tintColor = UIColor(white: 0.8, alpha: 1)
- }.onTouchUpInside { _ in
- print("Item Tapped")
- }
- }
-
- // MARK: - MessagesDataSource
-
- override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- if isTimeLabelVisible(at: indexPath) {
- return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray])
- }
- return nil
- }
-
- override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- if !isPreviousMessageSameSender(at: indexPath) {
- let name = message.sender.displayName
- return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
- return nil
- }
-
- override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
-
- if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) {
- return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
- return nil
- }
+// MARK: - AutocompleteExampleViewController
- // Async autocomplete requires the manager to reload
- func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) {
- guard autocompleteManager.currentSession != nil, autocompleteManager.currentSession?.prefix == "#" else { return }
- // Load some data asyncronously for the given session.prefix
- DispatchQueue.global(qos: .default).async {
- // fake background loading task
- var array: [AutocompleteCompletion] = []
- for _ in 1...10 {
- array.append(AutocompleteCompletion(text: Lorem.word()))
- }
- sleep(1)
- DispatchQueue.main.async { [weak self] in
- self?.asyncCompletions = array
- self?.autocompleteManager.reloadData()
- }
- }
- }
+final class AutocompleteExampleViewController: ChatViewController {
+ // MARK: Internal
+
+ lazy var joinChatButton: UIButton = {
+ let button = UIButton()
+ button.layer.cornerRadius = 16
+ button.backgroundColor = .primaryColor
+ button.setTitle("JOIN CHAT", for: .normal)
+ button.setTitleColor(.white, for: .normal)
+ button.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted)
+ button.addTarget(self, action: #selector(joinChat), for: .touchUpInside)
+ return button
+ }()
+
+ /// The object that manages autocomplete, from InputBarAccessoryView
+ lazy var autocompleteManager: AutocompleteManager = { [unowned self] in
+ let manager = AutocompleteManager(for: self.messageInputBar.inputTextView)
+ manager.delegate = self
+ manager.dataSource = self
+ return manager
+ }()
+
+ var hashtagAutocompletes: [AutocompleteCompletion] = {
+ var array: [AutocompleteCompletion] = []
+ for _ in 1 ... 100 {
+ array.append(AutocompleteCompletion(text: Lorem.word(), context: nil))
+ }
+ return array
+ }()
+
+ // Completions loaded async that get appended to local cached completions
+ var asyncCompletions: [AutocompleteCompletion] = []
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
+ .onTypingStatus { [weak self] in
+ self?.setTypingIndicatorViewHidden(false)
+ }.onNewMessage { [weak self] message in
+ self?.setTypingIndicatorViewHidden(true, performUpdates: {
+ self?.insertMessage(message)
+ })
+ }
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ messageInputBar.inputTextView.keyboardType = .twitter
+
+ // Configure AutocompleteManager
+ autocompleteManager.register(
+ prefix: "@",
+ with: [
+ .font: UIFont.preferredFont(forTextStyle: .body),
+ .foregroundColor: UIColor.primaryColor,
+ .backgroundColor: UIColor.primaryColor.withAlphaComponent(0.3),
+ ])
+ autocompleteManager.register(prefix: "#")
+ autocompleteManager.maxSpaceCountDuringCompletion = 1 // Allow for autocompletes with a space
+
+ // Set plugins
+ messageInputBar.inputPlugins = [autocompleteManager]
+ }
+
+ override func configureMessageCollectionView() {
+ super.configureMessageCollectionView()
+
+ let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
+ layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
+ layout?.setMessageOutgoingCellBottomLabelAlignment(.init(textAlignment: .right, textInsets: .zero))
+ layout?.setMessageOutgoingAvatarSize(.zero)
+ layout?
+ .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(
+ textAlignment: .right,
+ textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12)))
+ layout?
+ .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(
+ textAlignment: .right,
+ textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12)))
+
+ messagesCollectionView.messagesLayoutDelegate = self
+ messagesCollectionView.messagesDisplayDelegate = self
+
+ additionalBottomInset = 30
+ }
+
+ override func configureMessageInputBar() {
+ super.configureMessageInputBar()
+ messageInputBar.layer.shadowColor = UIColor.black.cgColor
+ messageInputBar.layer.shadowRadius = 4
+ messageInputBar.layer.shadowOpacity = 0.3
+ messageInputBar.layer.shadowOffset = CGSize(width: 0, height: 0)
+ messageInputBar.separatorLine.isHidden = true
+ messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false)
+ messageInputBar.setMiddleContentView(joinChatButton, animated: false)
+ }
+
+ @objc
+ func joinChat() {
+ configureMessageInputBarForChat()
+ }
+
+ // MARK: - Helpers
+
+ func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
+ indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
+ }
+
+ func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
+ guard indexPath.section - 1 >= 0 else { return false }
+ return messageList[indexPath.section].user == messageList[indexPath.section - 1].user
+ }
+
+ func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
+ guard indexPath.section + 1 < messageList.count else { return false }
+ return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
+ }
+
+ func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) {
+ setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in
+ if success, self?.isLastSectionVisible() == true {
+ self?.messagesCollectionView.scrollToLastItem(animated: true)
+ }
+ }
+ }
+
+ // MARK: - MessagesDataSource
+
+ override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if isTimeLabelVisible(at: indexPath) {
+ return NSAttributedString(
+ string: MessageKitDateFormatter.shared.string(from: message.sentDate),
+ attributes: [
+ NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
+ NSAttributedString.Key.foregroundColor: UIColor.darkGray,
+ ])
+ }
+ return nil
+ }
+
+ override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if !isPreviousMessageSameSender(at: indexPath) {
+ let name = message.sender.displayName
+ return NSAttributedString(
+ string: name,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+ return nil
+ }
+
+ override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if !isNextMessageSameSender(at: indexPath), isFromCurrentSender(message: message) {
+ return NSAttributedString(
+ string: "Delivered",
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+ return nil
+ }
+
+ // Async autocomplete requires the manager to reload
+ func inputBar(_: InputBarAccessoryView, textViewTextDidChangeTo _: String) {
+ guard autocompleteManager.currentSession != nil, autocompleteManager.currentSession?.prefix == "#" else { return }
+ // Load some data asyncronously for the given session.prefix
+ DispatchQueue.global(qos: .default).async {
+ // fake background loading task
+ var array: [AutocompleteCompletion] = []
+ for _ in 1 ... 10 {
+ array.append(AutocompleteCompletion(text: Lorem.word()))
+ }
+ sleep(1)
+ DispatchQueue.main.async { [weak self] in
+ self?.asyncCompletions = array
+ self?.autocompleteManager.reloadData()
+ }
+ }
+ }
+
+ // MARK: Private
+
+ private func configureMessageInputBarForChat() {
+ messageInputBar.setMiddleContentView(messageInputBar.inputTextView, animated: false)
+ messageInputBar.setRightStackViewWidthConstant(to: 52, animated: false)
+ let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), .flexibleSpace]
+ messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)
+
+ messageInputBar.sendButton.activityViewColor = .white
+ messageInputBar.sendButton.backgroundColor = .primaryColor
+ messageInputBar.sendButton.layer.cornerRadius = 10
+ messageInputBar.sendButton.setTitleColor(.white, for: .normal)
+ messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted)
+ messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .disabled)
+ messageInputBar.sendButton
+ .onSelected { item in
+ item.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
+ }.onDeselected { item in
+ item.transform = .identity
+ }
+ }
+
+ private func makeButton(named: String) -> InputBarButtonItem {
+ InputBarButtonItem()
+ .configure {
+ $0.spacing = .fixed(10)
+ $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
+ $0.setSize(CGSize(width: 25, height: 25), animated: false)
+ $0.tintColor = UIColor(white: 0.8, alpha: 1)
+ }.onSelected {
+ $0.tintColor = .primaryColor
+ }.onDeselected {
+ $0.tintColor = UIColor(white: 0.8, alpha: 1)
+ }.onTouchUpInside { _ in
+ print("Item Tapped")
+ }
+ }
}
-extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource {
+// MARK: AutocompleteManagerDelegate, AutocompleteManagerDataSource
- // MARK: - AutocompleteManagerDataSource
-
- func autocompleteManager(_ manager: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] {
-
- if prefix == "@" {
- return SampleData.shared.senders
- .map { user in
- return AutocompleteCompletion(text: user.displayName,
- context: ["id": user.senderId])
- }
- } else if prefix == "#" {
- return hastagAutocompletes + asyncCompletions
- }
- return []
- }
-
- func autocompleteManager(_ manager: AutocompleteManager, tableView: UITableView, cellForRowAt indexPath: IndexPath, for session: AutocompleteSession) -> UITableViewCell {
-
- guard let cell = tableView.dequeueReusableCell(withIdentifier: AutocompleteCell.reuseIdentifier, for: indexPath) as? AutocompleteCell else {
- fatalError("Oops, some unknown error occurred")
- }
- let users = SampleData.shared.senders
- let id = session.completion?.context?["id"] as? String
- let user = users.filter { return $0.senderId == id }.first
- if let sender = user {
- cell.imageView?.image = SampleData.shared.getAvatarFor(sender: sender).image
- }
- cell.imageViewEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
- cell.imageView?.layer.cornerRadius = 14
- cell.imageView?.layer.borderColor = UIColor.primaryColor.cgColor
- cell.imageView?.layer.borderWidth = 1
- cell.imageView?.clipsToBounds = true
- cell.textLabel?.attributedText = manager.attributedText(matching: session, fontSize: 15)
- return cell
- }
-
- // MARK: - AutocompleteManagerDelegate
-
- func autocompleteManager(_ manager: AutocompleteManager, shouldBecomeVisible: Bool) {
- setAutocompleteManager(active: shouldBecomeVisible)
- }
-
- // Optional
- func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool {
- return true
- }
-
- // Optional
- func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool {
- return true
- }
-
- // Optional
- func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool {
- return true
- }
-
- // MARK: - AutocompleteManagerDelegate Helper
-
- func setAutocompleteManager(active: Bool) {
- let topStackView = messageInputBar.topStackView
- if active && !topStackView.arrangedSubviews.contains(autocompleteManager.tableView) {
- topStackView.insertArrangedSubview(autocompleteManager.tableView, at: topStackView.arrangedSubviews.count)
- topStackView.layoutIfNeeded()
- } else if !active && topStackView.arrangedSubviews.contains(autocompleteManager.tableView) {
- topStackView.removeArrangedSubview(autocompleteManager.tableView)
- topStackView.layoutIfNeeded()
+extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource {
+ // MARK: - AutocompleteManagerDataSource
+
+ func autocompleteManager(_: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] {
+ if prefix == "@" {
+ return SampleData.shared.senders
+ .map { user in
+ AutocompleteCompletion(
+ text: user.displayName,
+ context: ["id": user.senderId])
}
- messageInputBar.invalidateIntrinsicContentSize()
- }
+ } else if prefix == "#" {
+ return hashtagAutocompletes + asyncCompletions
+ }
+ return []
+ }
+
+ func autocompleteManager(
+ _ manager: AutocompleteManager,
+ tableView: UITableView,
+ cellForRowAt indexPath: IndexPath,
+ for session: AutocompleteSession)
+ -> UITableViewCell
+ {
+ guard
+ let cell = tableView
+ .dequeueReusableCell(withIdentifier: AutocompleteCell.reuseIdentifier, for: indexPath) as? AutocompleteCell else
+ {
+ fatalError("Oops, some unknown error occurred")
+ }
+ let users = SampleData.shared.senders
+ let id = session.completion?.context?["id"] as? String
+ let user = users.filter { $0.senderId == id }.first
+ if let sender = user {
+ cell.imageView?.image = SampleData.shared.getAvatarFor(sender: sender).image
+ }
+ cell.imageViewEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
+ cell.imageView?.layer.cornerRadius = 14
+ cell.imageView?.layer.borderColor = UIColor.primaryColor.cgColor
+ cell.imageView?.layer.borderWidth = 1
+ cell.imageView?.clipsToBounds = true
+ cell.textLabel?.attributedText = manager.attributedText(matching: session, fontSize: 15)
+ return cell
+ }
+
+ // MARK: - AutocompleteManagerDelegate
+
+ func autocompleteManager(_: AutocompleteManager, shouldBecomeVisible: Bool) {
+ setAutocompleteManager(active: shouldBecomeVisible)
+ }
+
+ // Optional
+ func autocompleteManager(_: AutocompleteManager, shouldRegister _: String, at _: NSRange) -> Bool {
+ true
+ }
+
+ // Optional
+ func autocompleteManager(_: AutocompleteManager, shouldUnregister _: String) -> Bool {
+ true
+ }
+
+ // Optional
+ func autocompleteManager(_: AutocompleteManager, shouldComplete _: String, with _: String) -> Bool {
+ true
+ }
+
+ // MARK: - AutocompleteManagerDelegate Helper
+
+ func setAutocompleteManager(active: Bool) {
+ let topStackView = messageInputBar.topStackView
+ if active, !topStackView.arrangedSubviews.contains(autocompleteManager.tableView) {
+ topStackView.insertArrangedSubview(autocompleteManager.tableView, at: topStackView.arrangedSubviews.count)
+ topStackView.layoutIfNeeded()
+ } else if !active, topStackView.arrangedSubviews.contains(autocompleteManager.tableView) {
+ topStackView.removeArrangedSubview(autocompleteManager.tableView)
+ topStackView.layoutIfNeeded()
+ }
+ messageInputBar.invalidateIntrinsicContentSize()
+ }
}
-// MARK: - MessagesDisplayDelegate
+// MARK: MessagesDisplayDelegate
extension AutocompleteExampleViewController: MessagesDisplayDelegate {
-
- // MARK: - Text Messages
-
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .white : .darkText
- }
-
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
- switch detector {
- case .hashtag, .mention:
- if isFromCurrentSender(message: message) {
- return [.foregroundColor: UIColor.white]
- } else {
- return [.foregroundColor: UIColor.primaryColor]
- }
- default: return MessageLabel.defaultAttributes
- }
- }
-
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
- return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
- }
-
- // MARK: - All Messages
-
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
- }
-
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
- return .bubble
- }
-
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
- avatarView.set(avatar: avatar)
- avatarView.isHidden = isNextMessageSameSender(at: indexPath)
- avatarView.layer.borderWidth = 2
- avatarView.layer.borderColor = UIColor.primaryColor.cgColor
- }
-
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- // Cells are reused, so only add a button here once. For real use you would need to
- // ensure any subviews are removed if not needed
- accessoryView.subviews.forEach { $0.removeFromSuperview() }
-
- let button = UIButton(type: .infoLight)
- button.tintColor = .primaryColor
- accessoryView.addSubview(button)
- button.frame = accessoryView.bounds
- button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
- accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
- accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
- }
-
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
- imageView.kf.setImage(with: imageURL)
- } else {
- imageView.kf.cancelDownloadTask()
- }
- }
+ // MARK: - Text Messages
+
+ func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .white : .darkText
+ }
+
+ func detectorAttributes(
+ for detector: DetectorType,
+ and message: MessageType,
+ at _: IndexPath) -> [NSAttributedString.Key: Any]
+ {
+ switch detector {
+ case .hashtag, .mention:
+ if isFromCurrentSender(message: message) {
+ return [.foregroundColor: UIColor.white]
+ } else {
+ return [.foregroundColor: UIColor.primaryColor]
+ }
+ default: return MessageLabel.defaultAttributes
+ }
+ }
+
+ func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
+ [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
+ }
+
+ // MARK: - All Messages
+
+ func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
+ }
+
+ func messageStyle(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
+ .bubble
+ }
+
+ func configureAvatarView(
+ _ avatarView: AvatarView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in _: MessagesCollectionView)
+ {
+ let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
+ avatarView.set(avatar: avatar)
+ avatarView.isHidden = isNextMessageSameSender(at: indexPath)
+ avatarView.layer.borderWidth = 2
+ avatarView.layer.borderColor = UIColor.primaryColor.cgColor
+ }
+
+ func configureAccessoryView(_ accessoryView: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ // Cells are reused, so only add a button here once. For real use you would need to
+ // ensure any subviews are removed if not needed
+ accessoryView.subviews.forEach { $0.removeFromSuperview() }
+
+ let button = UIButton(type: .infoLight)
+ button.tintColor = .primaryColor
+ accessoryView.addSubview(button)
+ button.frame = accessoryView.bounds
+ button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
+ accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
+ accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
+ }
+
+ func configureMediaMessageImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ {
+ if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
+ imageView.kf.setImage(with: imageURL)
+ } else {
+ imageView.kf.cancelDownloadTask()
+ }
+ }
}
-// MARK: - MessagesLayoutDelegate
+// MARK: MessagesLayoutDelegate
extension AutocompleteExampleViewController: MessagesLayoutDelegate {
-
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- if isTimeLabelVisible(at: indexPath) {
- return 18
- }
- return 0
- }
-
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- if isFromCurrentSender(message: message) {
- return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
- } else {
- return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
- }
+ func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ if isTimeLabelVisible(at: indexPath) {
+ return 18
}
+ return 0
+ }
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
+ func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ if isFromCurrentSender(message: message) {
+ return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
+ } else {
+ return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
}
+ }
+ func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
+ }
}
diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift
index d43cb8d43..9f1994faa 100644
--- a/Example/Sources/View Controllers/BasicExampleViewController.swift
+++ b/Example/Sources/View Controllers/BasicExampleViewController.swift
@@ -1,140 +1,169 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
+import Kingfisher
import MapKit
import MessageKit
-import Kingfisher
+import UIKit
+
+// MARK: - BasicExampleViewController
class BasicExampleViewController: ChatViewController {
- override func configureMessageCollectionView() {
- super.configureMessageCollectionView()
- messagesCollectionView.messagesLayoutDelegate = self
- messagesCollectionView.messagesDisplayDelegate = self
- }
+ override func configureMessageCollectionView() {
+ super.configureMessageCollectionView()
+ messagesCollectionView.messagesLayoutDelegate = self
+ messagesCollectionView.messagesDisplayDelegate = self
+ }
+
+ func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator? {
+ nil
+ }
}
-// MARK: - MessagesDisplayDelegate
+// MARK: MessagesDisplayDelegate
extension BasicExampleViewController: MessagesDisplayDelegate {
-
- // MARK: - Text Messages
-
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .white : .darkText
- }
-
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
- switch detector {
- case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
- default: return MessageLabel.defaultAttributes
- }
- }
-
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
- return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
- }
-
- // MARK: - All Messages
-
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
+ // MARK: - Text Messages
+
+ func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .white : .darkText
+ }
+
+ func detectorAttributes(for detector: DetectorType, and _: MessageType, at _: IndexPath) -> [NSAttributedString.Key: Any] {
+ switch detector {
+ case .hashtag, .mention: return [.foregroundColor: UIColor.blue]
+ default: return MessageLabel.defaultAttributes
}
-
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
-
- let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
+ }
+
+ func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
+ [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
+ }
+
+ // MARK: - All Messages
+
+ func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
+ }
+
+ func messageStyle(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
+ let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
+ if let image = UIImage(named: "bobbly") {
+ return .customImageTail(image, tail)
+ } else {
return .bubbleTail(tail, .curved)
}
-
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
- avatarView.set(avatar: avatar)
- }
+ }
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
- imageView.kf.setImage(with: imageURL)
- } else {
- imageView.kf.cancelDownloadTask()
- }
- }
-
- // MARK: - Location Messages
-
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
- let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
- let pinImage = #imageLiteral(resourceName: "ic_map_marker")
- annotationView.image = pinImage
- annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
- return annotationView
- }
-
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
- return { view in
- view.layer.transform = CATransform3DMakeScale(2, 2, 2)
- UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
- view.layer.transform = CATransform3DIdentity
- }, completion: nil)
- }
- }
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
-
- return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
- }
+ func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
+ avatarView.set(avatar: avatar)
+ }
- // MARK: - Audio Messages
+ func configureMediaMessageImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ {
+ if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
+ imageView.kf.setImage(with: imageURL)
+ } else {
+ imageView.kf.cancelDownloadTask()
+ }
+ }
- func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- return isFromCurrentSender(message: message) ? .white : UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0)
- }
-
- func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
- audioController.configureAudioCell(cell, message: message) // this is needed especily when the cell is reconfigure while is playing sound
- }
+ // MARK: - Location Messages
+
+ func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? {
+ let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
+ let pinImage = #imageLiteral(resourceName: "ic_map_marker")
+ annotationView.image = pinImage
+ annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
+ return annotationView
+ }
+
+ func animationBlockForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
+ {
+ { view in
+ view.layer.transform = CATransform3DMakeScale(2, 2, 2)
+ UIView.animate(
+ withDuration: 0.6,
+ delay: 0,
+ usingSpringWithDamping: 0.9,
+ initialSpringVelocity: 0,
+ options: [],
+ animations: {
+ view.layer.transform = CATransform3DIdentity
+ },
+ completion: nil)
+ }
+ }
+
+ func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions(
+ showsBuildings: true,
+ showsPointsOfInterest: true,
+ span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
+ }
+
+ // MARK: - Audio Messages
+ func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
+ isFromCurrentSender(message: message) ? .white : UIColor(red: 15 / 255, green: 135 / 255, blue: 255 / 255, alpha: 1.0)
+ }
+
+ func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
+ audioController
+ .configureAudioCell(
+ cell,
+ message: message) // this is needed especially when the cell is reconfigure while is playing sound
+ }
}
-// MARK: - MessagesLayoutDelegate
+// MARK: MessagesLayoutDelegate
extension BasicExampleViewController: MessagesLayoutDelegate {
-
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 18
- }
-
- func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 17
- }
-
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 20
- }
-
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 16
- }
-
+ func cellTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 18
+ }
+
+ func cellBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 17
+ }
+
+ func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 20
+ }
+
+ func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 16
+ }
}
diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift
index b5e71b8b4..aeb2171ae 100644
--- a/Example/Sources/View Controllers/ChatViewController.swift
+++ b/Example/Sources/View Controllers/ChatViewController.swift
@@ -1,355 +1,372 @@
-/*
-MIT License
-
-Copyright (c) 2017-2020 MessageKit
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
+//
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
-import MessageKit
import InputBarAccessoryView
+import MessageKit
+import UIKit
+
+// MARK: - ChatViewController
/// A base class for the example controllers
class ChatViewController: MessagesViewController, MessagesDataSource {
-
- // MARK: - Public properties
-
- /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and udpate audio cell UI accordingly.
- lazy var audioController = BasicAudioController(messageCollectionView: messagesCollectionView)
-
- lazy var messageList: [MockMessage] = []
-
- private(set) lazy var refreshControl: UIRefreshControl = {
- let control = UIRefreshControl()
- control.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
- return control
- }()
-
- // MARK: - Private properties
-
- private let formatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .medium
- return formatter
- }()
-
- // MARK: - Lifecycle
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- configureMessageCollectionView()
- configureMessageInputBar()
- loadFirstMessages()
- title = "MessageKit"
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
- .onNewMessage { [weak self] message in
- self?.insertMessage(message)
- }
- }
-
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- MockSocket.shared.disconnect()
- audioController.stopAnyOngoingPlaying()
- }
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
-
- func loadFirstMessages() {
- DispatchQueue.global(qos: .userInitiated).async {
- let count = UserDefaults.standard.mockMessagesCount()
- SampleData.shared.getMessages(count: count) { messages in
- DispatchQueue.main.async {
- self.messageList = messages
- self.messagesCollectionView.reloadData()
- self.messagesCollectionView.scrollToLastItem()
- }
- }
- }
- }
-
- @objc func loadMoreMessages() {
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
- SampleData.shared.getMessages(count: 20) { messages in
- DispatchQueue.main.async {
- self.messageList.insert(contentsOf: messages, at: 0)
- self.messagesCollectionView.reloadDataAndKeepOffset()
- self.refreshControl.endRefreshing()
- }
- }
+ // MARK: Internal
+
+ // MARK: - Public properties
+
+ /// The `BasicAudioController` control the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly.
+ lazy var audioController = BasicAudioController(messageCollectionView: messagesCollectionView)
+
+ lazy var messageList: [MockMessage] = []
+
+ private(set) lazy var refreshControl: UIRefreshControl = {
+ let control = UIRefreshControl()
+ control.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
+ return control
+ }()
+
+ // MARK: - MessagesDataSource
+
+ var currentSender: SenderType {
+ SampleData.shared.currentSender
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ navigationItem.title = "MessageKit"
+
+ configureMessageCollectionView()
+ configureMessageInputBar()
+ loadFirstMessages()
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
+ .onNewMessage { [weak self] message in
+ self?.insertMessage(message)
+ }
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ MockSocket.shared.disconnect()
+ audioController.stopAnyOngoingPlaying()
+ }
+
+ func loadFirstMessages() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ let count = UserDefaults.standard.mockMessagesCount()
+ SampleData.shared.getMessages(count: count) { messages in
+ DispatchQueue.main.async {
+ self.messageList = messages
+ self.messagesCollectionView.reloadData()
+ self.messagesCollectionView.scrollToLastItem(animated: false)
}
- }
-
- func configureMessageCollectionView() {
-
- messagesCollectionView.messagesDataSource = self
- messagesCollectionView.messageCellDelegate = self
-
- scrollsToLastItemOnKeyboardBeginsEditing = true // default false
- maintainPositionOnKeyboardFrameChanged = true // default false
-
- showMessageTimestampOnSwipeLeft = true // default false
-
- messagesCollectionView.refreshControl = refreshControl
- }
-
- func configureMessageInputBar() {
- messageInputBar.delegate = self
- messageInputBar.inputTextView.tintColor = .primaryColor
- messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal)
- messageInputBar.sendButton.setTitleColor(
- UIColor.primaryColor.withAlphaComponent(0.3),
- for: .highlighted
- )
- }
-
- // MARK: - Helpers
-
- func insertMessage(_ message: MockMessage) {
- messageList.append(message)
- // Reload last section to update header/footer labels and insert a new one
- messagesCollectionView.performBatchUpdates({
- messagesCollectionView.insertSections([messageList.count - 1])
- if messageList.count >= 2 {
- messagesCollectionView.reloadSections([messageList.count - 2])
- }
- }, completion: { [weak self] _ in
- if self?.isLastSectionVisible() == true {
- self?.messagesCollectionView.scrollToLastItem(animated: true)
- }
- })
- }
-
- func isLastSectionVisible() -> Bool {
-
- guard !messageList.isEmpty else { return false }
-
- let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1)
-
- return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
- }
-
- // MARK: - MessagesDataSource
-
- func currentSender() -> SenderType {
- return SampleData.shared.currentSender
- }
-
- func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
- return messageList.count
- }
-
- func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
- return messageList[indexPath.section]
- }
-
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- if indexPath.section % 3 == 0 {
- return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray])
+ }
+ }
+ }
+
+ @objc
+ func loadMoreMessages() {
+ DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
+ SampleData.shared.getMessages(count: 20) { messages in
+ DispatchQueue.main.async {
+ self.messageList.insert(contentsOf: messages, at: 0)
+ self.messagesCollectionView.reloadDataAndKeepOffset()
+ self.refreshControl.endRefreshing()
}
- return nil
- }
-
- func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return NSAttributedString(string: "Read", attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray])
- }
-
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let name = message.sender.displayName
- return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
-
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let dateString = formatter.string(from: message.sentDate)
- return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
- }
+ }
+ }
+ }
+
+ func configureMessageCollectionView() {
+ messagesCollectionView.messagesDataSource = self
+ messagesCollectionView.messageCellDelegate = self
+
+ scrollsToLastItemOnKeyboardBeginsEditing = true // default false
+ maintainPositionOnInputBarHeightChanged = true // default false
+ showMessageTimestampOnSwipeLeft = true // default false
+
+ messagesCollectionView.refreshControl = refreshControl
+ }
+
+ func configureMessageInputBar() {
+ messageInputBar.delegate = self
+ messageInputBar.inputTextView.tintColor = .primaryColor
+ messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal)
+ messageInputBar.sendButton.setTitleColor(
+ UIColor.primaryColor.withAlphaComponent(0.3),
+ for: .highlighted)
+ }
+
+ // MARK: - Helpers
+
+ func insertMessage(_ message: MockMessage) {
+ messageList.append(message)
+ // Reload last section to update header/footer labels and insert a new one
+ messagesCollectionView.performBatchUpdates({
+ messagesCollectionView.insertSections([messageList.count - 1])
+ if messageList.count >= 2 {
+ messagesCollectionView.reloadSections([messageList.count - 2])
+ }
+ }, completion: { [weak self] _ in
+ if self?.isLastSectionVisible() == true {
+ self?.messagesCollectionView.scrollToLastItem(animated: true)
+ }
+ })
+ }
+
+ func isLastSectionVisible() -> Bool {
+ guard !messageList.isEmpty else { return false }
+
+ let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1)
+
+ return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
+ }
+
+ func numberOfSections(in _: MessagesCollectionView) -> Int {
+ messageList.count
+ }
+
+ func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
+ messageList[indexPath.section]
+ }
+
+ func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
+ if indexPath.section % 3 == 0 {
+ return NSAttributedString(
+ string: MessageKitDateFormatter.shared.string(from: message.sentDate),
+ attributes: [
+ NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
+ NSAttributedString.Key.foregroundColor: UIColor.darkGray,
+ ])
+ }
+ return nil
+ }
+
+ func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ NSAttributedString(
+ string: "Read",
+ attributes: [
+ NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
+ NSAttributedString.Key.foregroundColor: UIColor.darkGray,
+ ])
+ }
+
+ func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let name = message.sender.displayName
+ return NSAttributedString(
+ string: name,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+
+ func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let dateString = formatter.string(from: message.sentDate)
+ return NSAttributedString(
+ string: dateString,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
+ }
+
+ func textCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ // MARK: Private
+
+ // MARK: - Private properties
+
+ private let formatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ return formatter
+ }()
}
-// MARK: - MessageCellDelegate
+// MARK: MessageCellDelegate
extension ChatViewController: MessageCellDelegate {
- func didTapAvatar(in cell: MessageCollectionViewCell) {
- print("Avatar tapped")
- }
-
- func didTapMessage(in cell: MessageCollectionViewCell) {
- print("Message tapped")
- }
-
- func didTapImage(in cell: MessageCollectionViewCell) {
- print("Image tapped")
- }
-
- func didTapCellTopLabel(in cell: MessageCollectionViewCell) {
- print("Top cell label tapped")
- }
-
- func didTapCellBottomLabel(in cell: MessageCollectionViewCell) {
- print("Bottom cell label tapped")
- }
-
- func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {
- print("Top message label tapped")
- }
-
- func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {
- print("Bottom label tapped")
- }
-
- func didTapPlayButton(in cell: AudioMessageCell) {
- guard let indexPath = messagesCollectionView.indexPath(for: cell),
- let message = messagesCollectionView.messagesDataSource?.messageForItem(at: indexPath, in: messagesCollectionView) else {
- print("Failed to identify message when audio cell receive tap gesture")
- return
- }
- guard audioController.state != .stopped else {
- // There is no audio sound playing - prepare to start playing for given audio message
- audioController.playSound(for: message, in: cell)
- return
- }
- if audioController.playingMessage?.messageId == message.messageId {
- // tap occur in the current cell that is playing audio sound
- if audioController.state == .playing {
- audioController.pauseSound(for: message, in: cell)
- } else {
- audioController.resumeSound()
- }
- } else {
- // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message
- audioController.stopAnyOngoingPlaying()
- audioController.playSound(for: message, in: cell)
- }
- }
-
- func didStartAudio(in cell: AudioMessageCell) {
- print("Did start playing audio sound")
- }
-
- func didPauseAudio(in cell: AudioMessageCell) {
- print("Did pause audio sound")
- }
-
- func didStopAudio(in cell: AudioMessageCell) {
- print("Did stop audio sound")
- }
-
- func didTapAccessoryView(in cell: MessageCollectionViewCell) {
- print("Accessory view tapped")
- }
-
+ func didTapAvatar(in _: MessageCollectionViewCell) {
+ print("Avatar tapped")
+ }
+
+ func didTapMessage(in _: MessageCollectionViewCell) {
+ print("Message tapped")
+ }
+
+ func didTapImage(in _: MessageCollectionViewCell) {
+ print("Image tapped")
+ }
+
+ func didTapCellTopLabel(in _: MessageCollectionViewCell) {
+ print("Top cell label tapped")
+ }
+
+ func didTapCellBottomLabel(in _: MessageCollectionViewCell) {
+ print("Bottom cell label tapped")
+ }
+
+ func didTapMessageTopLabel(in _: MessageCollectionViewCell) {
+ print("Top message label tapped")
+ }
+
+ func didTapMessageBottomLabel(in _: MessageCollectionViewCell) {
+ print("Bottom label tapped")
+ }
+
+ func didTapPlayButton(in cell: AudioMessageCell) {
+ guard
+ let indexPath = messagesCollectionView.indexPath(for: cell),
+ let message = messagesCollectionView.messagesDataSource?.messageForItem(at: indexPath, in: messagesCollectionView)
+ else {
+ print("Failed to identify message when audio cell receive tap gesture")
+ return
+ }
+ guard audioController.state != .stopped else {
+ // There is no audio sound playing - prepare to start playing for given audio message
+ audioController.playSound(for: message, in: cell)
+ return
+ }
+ if audioController.playingMessage?.messageId == message.messageId {
+ // tap occur in the current cell that is playing audio sound
+ if audioController.state == .playing {
+ audioController.pauseSound(for: message, in: cell)
+ } else {
+ audioController.resumeSound()
+ }
+ } else {
+ // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message
+ audioController.stopAnyOngoingPlaying()
+ audioController.playSound(for: message, in: cell)
+ }
+ }
+
+ func didStartAudio(in _: AudioMessageCell) {
+ print("Did start playing audio sound")
+ }
+
+ func didPauseAudio(in _: AudioMessageCell) {
+ print("Did pause audio sound")
+ }
+
+ func didStopAudio(in _: AudioMessageCell) {
+ print("Did stop audio sound")
+ }
+
+ func didTapAccessoryView(in _: MessageCollectionViewCell) {
+ print("Accessory view tapped")
+ }
}
-// MARK: - MessageLabelDelegate
+// MARK: MessageLabelDelegate
extension ChatViewController: MessageLabelDelegate {
- func didSelectAddress(_ addressComponents: [String: String]) {
- print("Address Selected: \(addressComponents)")
- }
-
- func didSelectDate(_ date: Date) {
- print("Date Selected: \(date)")
- }
-
- func didSelectPhoneNumber(_ phoneNumber: String) {
- print("Phone Number Selected: \(phoneNumber)")
- }
-
- func didSelectURL(_ url: URL) {
- print("URL Selected: \(url)")
- }
-
- func didSelectTransitInformation(_ transitInformation: [String: String]) {
- print("TransitInformation Selected: \(transitInformation)")
- }
+ func didSelectAddress(_ addressComponents: [String: String]) {
+ print("Address Selected: \(addressComponents)")
+ }
- func didSelectHashtag(_ hashtag: String) {
- print("Hashtag selected: \(hashtag)")
- }
+ func didSelectDate(_ date: Date) {
+ print("Date Selected: \(date)")
+ }
- func didSelectMention(_ mention: String) {
- print("Mention selected: \(mention)")
- }
-
- func didSelectCustom(_ pattern: String, match: String?) {
- print("Custom data detector patter selected: \(pattern)")
- }
-}
+ func didSelectPhoneNumber(_ phoneNumber: String) {
+ print("Phone Number Selected: \(phoneNumber)")
+ }
-// MARK: - MessageInputBarDelegate
+ func didSelectURL(_ url: URL) {
+ print("URL Selected: \(url)")
+ }
-extension ChatViewController: InputBarAccessoryViewDelegate {
+ func didSelectTransitInformation(_ transitInformation: [String: String]) {
+ print("TransitInformation Selected: \(transitInformation)")
+ }
- @objc
- func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
- processInputBar(messageInputBar)
- }
+ func didSelectHashtag(_ hashtag: String) {
+ print("Hashtag selected: \(hashtag)")
+ }
- func processInputBar(_ inputBar: InputBarAccessoryView) {
- // Here we can parse for which substrings were autocompleted
- let attributedText = inputBar.inputTextView.attributedText!
- let range = NSRange(location: 0, length: attributedText.length)
- attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { (_, range, _) in
+ func didSelectMention(_ mention: String) {
+ print("Mention selected: \(mention)")
+ }
- let substring = attributedText.attributedSubstring(from: range)
- let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil)
- print("Autocompleted: `", substring, "` with context: ", context ?? [])
- }
+ func didSelectCustom(_ pattern: String, match _: String?) {
+ print("Custom data detector patter selected: \(pattern)")
+ }
+}
- let components = inputBar.inputTextView.components
- inputBar.inputTextView.text = String()
- inputBar.invalidatePlugins()
- // Send button activity animation
- inputBar.sendButton.startAnimating()
- inputBar.inputTextView.placeholder = "Sending..."
- // Resign first responder for iPad split view
- inputBar.inputTextView.resignFirstResponder()
- DispatchQueue.global(qos: .default).async {
- // fake send request task
- sleep(1)
- DispatchQueue.main.async { [weak self] in
- inputBar.sendButton.stopAnimating()
- inputBar.inputTextView.placeholder = "Aa"
- self?.insertMessages(components)
- self?.messagesCollectionView.scrollToLastItem(animated: true)
- }
- }
- }
+// MARK: InputBarAccessoryViewDelegate
- private func insertMessages(_ data: [Any]) {
- for component in data {
- let user = SampleData.shared.currentSender
- if let str = component as? String {
- let message = MockMessage(text: str, user: user, messageId: UUID().uuidString, date: Date())
- insertMessage(message)
- } else if let img = component as? UIImage {
- let message = MockMessage(image: img, user: user, messageId: UUID().uuidString, date: Date())
- insertMessage(message)
- }
- }
- }
+extension ChatViewController: InputBarAccessoryViewDelegate {
+ // MARK: Internal
+
+ @objc
+ func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) {
+ processInputBar(messageInputBar)
+ }
+
+ func processInputBar(_ inputBar: InputBarAccessoryView) {
+ // Here we can parse for which substrings were autocompleted
+ let attributedText = inputBar.inputTextView.attributedText!
+ let range = NSRange(location: 0, length: attributedText.length)
+ attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { _, range, _ in
+
+ let substring = attributedText.attributedSubstring(from: range)
+ let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil)
+ print("Autocompleted: `", substring, "` with context: ", context ?? "-")
+ }
+
+ let components = inputBar.inputTextView.components
+ inputBar.inputTextView.text = String()
+ inputBar.invalidatePlugins()
+ // Send button activity animation
+ inputBar.sendButton.startAnimating()
+ inputBar.inputTextView.placeholder = "Sending..."
+ // Resign first responder for iPad split view
+ inputBar.inputTextView.resignFirstResponder()
+ DispatchQueue.global(qos: .default).async {
+ // fake send request task
+ sleep(1)
+ DispatchQueue.main.async { [weak self] in
+ inputBar.sendButton.stopAnimating()
+ inputBar.inputTextView.placeholder = "Aa"
+ self?.insertMessages(components)
+ self?.messagesCollectionView.scrollToLastItem(animated: true)
+ }
+ }
+ }
+
+ // MARK: Private
+
+ private func insertMessages(_ data: [Any]) {
+ for component in data {
+ let user = SampleData.shared.currentSender
+ if let str = component as? String {
+ let message = MockMessage(text: str, user: user, messageId: UUID().uuidString, date: Date())
+ insertMessage(message)
+ } else if let img = component as? UIImage {
+ let message = MockMessage(image: img, user: user, messageId: UUID().uuidString, date: Date())
+ insertMessage(message)
+ }
+ }
+ }
}
diff --git a/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift b/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift
new file mode 100644
index 000000000..a3c148269
--- /dev/null
+++ b/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift
@@ -0,0 +1,49 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+final class CustomInputBarExampleViewController: BasicExampleViewController {
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let customInputView = UIView()
+ customInputView.backgroundColor = .secondarySystemBackground
+ let customLabel = UILabel()
+ customLabel.translatesAutoresizingMaskIntoConstraints = false
+ customLabel.font = .preferredFont(forTextStyle: .headline)
+ customLabel.textAlignment = .center
+ customLabel.text = "This chat is read only."
+ customLabel.textColor = .primaryColor
+ customInputView.addSubview(customLabel)
+
+ NSLayoutConstraint.activate([
+ customLabel.topAnchor.constraint(equalTo: customInputView.topAnchor, constant: 16),
+ customLabel.bottomAnchor.constraint(equalTo: customInputView.safeAreaLayoutGuide.bottomAnchor, constant: -16),
+ customLabel.leadingAnchor.constraint(equalTo: customInputView.leadingAnchor),
+ customLabel.trailingAnchor.constraint(equalTo: customInputView.trailingAnchor),
+ ])
+
+ inputBarType = .custom(customInputView)
+ }
+}
diff --git a/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift b/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift
new file mode 100644
index 000000000..bd5281109
--- /dev/null
+++ b/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift
@@ -0,0 +1,62 @@
+//
+// CustomLayoutExampleViewController.swift
+// ChatExample
+//
+// Created by Vignesh J on 30/04/21.
+// Copyright © 2021 MessageKit. All rights reserved.
+//
+
+import Kingfisher
+import MapKit
+import MessageKit
+import UIKit
+
+class CustomLayoutExampleViewController: BasicExampleViewController {
+ // MARK: Internal
+
+ override func configureMessageCollectionView() {
+ super.configureMessageCollectionView()
+ messagesCollectionView.register(CustomTextMessageContentCell.self)
+ messagesCollectionView.messagesDataSource = self
+ messagesCollectionView.messagesLayoutDelegate = self
+ messagesCollectionView.messagesDisplayDelegate = self
+ }
+
+ // MARK: - MessagesLayoutDelegate
+
+ override func textCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ textMessageSizeCalculator
+ }
+
+ // MARK: - MessagesDataSource
+
+ override func textCell(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+ {
+ let cell = messagesCollectionView.dequeueReusableCell(
+ CustomTextMessageContentCell.self,
+ for: indexPath)
+ cell.configure(
+ with: message,
+ at: indexPath,
+ in: messagesCollectionView,
+ dataSource: self,
+ and: textMessageSizeCalculator)
+
+ return cell
+ }
+
+ // MARK: Private
+
+ private lazy var textMessageSizeCalculator = CustomTextLayoutSizeCalculator(
+ layout: self.messagesCollectionView
+ .messagesCollectionViewFlowLayout)
+}
diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift
index 55d110d8c..90c42b894 100644
--- a/Example/Sources/View Controllers/LaunchViewController.swift
+++ b/Example/Sources/View Controllers/LaunchViewController.swift
@@ -1,120 +1,162 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MessageKit
import SafariServices
import SwiftUI
+import UIKit
final internal class LaunchViewController: UITableViewController {
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
+ // MARK: Lifecycle
- let cells = ["Basic Example", "Advanced Example", "Autocomplete Example", "Embedded Example", "Subview Example", "SwiftUI Example", "Settings", "Source Code", "Contributors"]
-
- // MARK: - View Life Cycle
-
- override func viewDidLoad() {
- super.viewDidLoad()
- title = "MessageKit"
- navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
- tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
- tableView.tableFooterView = UIView()
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- navigationController?.navigationBar.prefersLargeTitles = true
- }
-
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- navigationController?.navigationBar.prefersLargeTitles = false
- }
-
- // MARK: - UITableViewDataSource
+ // MARK: - View Life Cycle
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return cells.count
- }
+ init() {
+ super.init(style: .insetGrouped)
+ }
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()
- cell.textLabel?.text = cells[indexPath.row]
- cell.accessoryType = .disclosureIndicator
- return cell
- }
-
- // MARK: - UITableViewDelegate
-
- // swiftlint:disable cyclomatic_complexity
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let cell = cells[indexPath.row]
- switch cell {
- case "Basic Example":
- let viewController = BasicExampleViewController()
- let detailViewController = NavigationController(rootViewController: viewController)
- splitViewController?.showDetailViewController(detailViewController, sender: self)
- case "Advanced Example":
- let viewController = AdvancedExampleViewController()
- let detailViewController = NavigationController(rootViewController: viewController)
- splitViewController?.showDetailViewController(detailViewController, sender: self)
- case "Autocomplete Example":
- let viewController = AutocompleteExampleViewController()
- let detailViewController = NavigationController(rootViewController: viewController)
- splitViewController?.showDetailViewController(detailViewController, sender: self)
- case "Embedded Example":
- navigationController?.pushViewController(MessageContainerController(), animated: true)
- case "SwiftUI Example":
- if #available(iOS 13, *) {
- navigationController?.pushViewController(UIHostingController(rootView: SwiftUIExampleView()), animated: true)
- }
- case "Settings":
- let viewController = SettingsViewController()
- let detailViewController = NavigationController(rootViewController: viewController)
- splitViewController?.showDetailViewController(detailViewController, sender: self)
- case "Subview Example":
- let viewController = MessageSubviewContainerViewController()
- let detailViewController = NavigationController(rootViewController: viewController)
- splitViewController?.showDetailViewController(detailViewController, sender: self)
- case "Source Code":
- guard let url = URL(string: "https://github.com/MessageKit/MessageKit") else { return }
- openURL(url)
- case "Contributors":
- guard let url = URL(string: "https://github.com/orgs/MessageKit/teams/contributors/members") else { return }
- openURL(url)
- default:
- assertionFailure("You need to impliment the action for this cell: \(cell)")
- return
- }
+ required init?(coder _: NSCoder) { nil }
+
+ // MARK: Internal
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = "MessageKit"
+ navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
+ navigationController?.navigationBar.tintColor = .primaryColor
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
+ tableView.tableFooterView = UIView()
+ }
+
+ // MARK: - UITableViewDataSource
+
+ override func numberOfSections(in _: UITableView) -> Int {
+ sections.count
+ }
+
+ override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
+ sections[section].rows.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()
+ cell.textLabel?.text = sections[indexPath.section].rows[indexPath.row].title
+ cell.accessoryType = .disclosureIndicator
+ return cell
+ }
+
+ // MARK: - UITableViewDelegate
+
+ // swiftlint:disable cyclomatic_complexity
+ override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let cell = sections[indexPath.section].rows[indexPath.row]
+ switch cell {
+ case .basic:
+ let viewController = BasicExampleViewController()
+ let detailViewController = UINavigationController(rootViewController: viewController)
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .advanced:
+ let viewController = AdvancedExampleViewController()
+ let detailViewController = UINavigationController(rootViewController: viewController)
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .autocomplete:
+ let viewController = AutocompleteExampleViewController()
+ let detailViewController = UINavigationController(rootViewController: viewController)
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .embedded:
+ splitViewController?.showDetailViewController(MessageContainerController(), sender: self)
+ case .customLayout:
+ splitViewController?.showDetailViewController(CustomLayoutExampleViewController(), sender: self)
+ case .customInputBar:
+ let detailViewController = UINavigationController(rootViewController: CustomInputBarExampleViewController())
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .swiftUI:
+ splitViewController?.showDetailViewController(UIHostingController(rootView: SwiftUIExampleView()), sender: self)
+ case .subview:
+ let viewController = MessageSubviewContainerViewController()
+ let detailViewController = UINavigationController(rootViewController: viewController)
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .settings:
+ let viewController = SettingsViewController()
+ let detailViewController = UINavigationController(rootViewController: viewController)
+ splitViewController?.showDetailViewController(detailViewController, sender: self)
+ case .sourceCode:
+ openURL(URL(string: "https://github.com/MessageKit/MessageKit")!)
+ case .contributors:
+ openURL(URL(string: "https://github.com/MessageKit/MessageKit/graphs/contributors")!)
}
-
- func openURL(_ url: URL) {
- let webViewController = SFSafariViewController(url: url)
- webViewController.preferredControlTintColor = .primaryColor
- splitViewController?.showDetailViewController(webViewController, sender: self)
+ }
+
+ func openURL(_ url: URL) {
+ let webViewController = SFSafariViewController(url: url)
+ webViewController.preferredControlTintColor = .primaryColor
+ present(webViewController, animated: true)
+ }
+
+ // MARK: Private
+
+ private enum Row {
+ case basic, advanced, autocomplete, embedded, customLayout, subview, customInputBar, swiftUI
+ case settings, sourceCode, contributors
+
+ // MARK: Internal
+
+ var title: String {
+ switch self {
+ case .basic:
+ return "Basic Example"
+ case .advanced:
+ return "Advanced Example"
+ case .autocomplete:
+ return "Autocomplete Example"
+ case .embedded:
+ return "Embedded Example"
+ case .customLayout:
+ return "Custom Layout Example"
+ case .subview:
+ return "Subview Example"
+ case .customInputBar:
+ return "Custom InputBar Example"
+ case .swiftUI:
+ return "SwiftUI Example"
+ case .settings:
+ return "Settings"
+ case .sourceCode:
+ return "Source Code"
+ case .contributors:
+ return "Contributors"
+ }
}
+ }
+
+ private struct Section {
+ let title: String
+ let rows: [Row]
+ }
+
+ private let sections: [Section] = [
+ .init(
+ title: "Examples",
+ rows: [.basic, .advanced, .autocomplete, .embedded, .customLayout, .subview, .customInputBar, .swiftUI]),
+ .init(title: "Support", rows: [.settings, .sourceCode, .contributors]),
+ ]
}
diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift
index e48d65840..3fd11c7e0 100644
--- a/Example/Sources/View Controllers/MessageContainerController.swift
+++ b/Example/Sources/View Controllers/MessageContainerController.swift
@@ -1,88 +1,71 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MapKit
+import UIKit
final class MessageContainerController: UIViewController {
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
-
- let mapView = MKMapView()
-
- let bannerView: UIView = {
- let view = UIView()
- view.backgroundColor = .primaryColor
- view.alpha = 0.7
- return view
- }()
-
- let conversationViewController = BasicExampleViewController()
-
- /// Required for the `MessageInputBar` to be visible
- override var canBecomeFirstResponder: Bool {
- return conversationViewController.canBecomeFirstResponder
- }
-
- /// Required for the `MessageInputBar` to be visible
- override var inputAccessoryView: UIView? {
- return conversationViewController.inputAccessoryView
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- /// Add the `ConversationViewController` as a child view controller
- conversationViewController.willMove(toParent: self)
- addChild(conversationViewController)
- view.addSubview(conversationViewController.view)
- conversationViewController.didMove(toParent: self)
-
- view.addSubview(mapView)
- 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
- mapView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight))
- bannerView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight))
- conversationViewController.view.frame = CGRect(x: 0, y: headerHeight, width: view.bounds.width, height: view.bounds.height - headerHeight)
- }
-
+ let mapView = MKMapView()
+
+ let bannerView: UIView = {
+ let view = UIView()
+ view.backgroundColor = .primaryColor
+ view.alpha = 0.7
+ return view
+ }()
+
+ let conversationViewController = BasicExampleViewController()
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ .lightContent
+ }
+
+ /// Required for the `MessageInputBar` to be visible
+ override var canBecomeFirstResponder: Bool {
+ conversationViewController.canBecomeFirstResponder
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ /// Add the `ConversationViewController` as a child view controller
+ conversationViewController.willMove(toParent: self)
+ addChild(conversationViewController)
+ view.addSubview(conversationViewController.view)
+ conversationViewController.didMove(toParent: self)
+
+ view.addSubview(mapView)
+ view.addSubview(bannerView)
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+ let headerHeight: CGFloat = 200
+ mapView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight))
+ bannerView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight))
+ conversationViewController.view.frame = CGRect(
+ x: 0,
+ y: headerHeight,
+ width: view.bounds.width,
+ height: view.bounds.height - headerHeight)
+ }
}
diff --git a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift
index 4ddbb3737..ed6e116c0 100644
--- a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift
+++ b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift
@@ -1,57 +1,42 @@
-/*
-MIT License
-
-Copyright (c) 2017-2019 MessageKit
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
+//
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
final class MessageSubviewContainerViewController: UIViewController {
+ let messageSubviewViewController = MessageSubviewViewController()
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
-
- let messageSubviewViewController = MessageSubviewViewController()
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- messageSubviewViewController.willMove(toParent: self)
- addChild(messageSubviewViewController)
- 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 var preferredStatusBarStyle: UIStatusBarStyle {
+ .lightContent
+ }
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- navigationController?.navigationBar.isTranslucent = false
- navigationController?.navigationBar.barTintColor = .primaryColor
- }
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ messageSubviewViewController.willMove(toParent: self)
+ addChild(messageSubviewViewController)
+ view.addSubview(messageSubviewViewController.view)
+ messageSubviewViewController.didMove(toParent: self)
+ }
}
diff --git a/Example/Sources/View Controllers/MessageSubviewViewController.swift b/Example/Sources/View Controllers/MessageSubviewViewController.swift
index 0481d5e34..04d128891 100644
--- a/Example/Sources/View Controllers/MessageSubviewViewController.swift
+++ b/Example/Sources/View Controllers/MessageSubviewViewController.swift
@@ -1,51 +1,60 @@
-/*
-MIT License
-
-Copyright (c) 2017-2019 MessageKit
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
+//
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-import UIKit
import InputBarAccessoryView
+import UIKit
final class MessageSubviewViewController: BasicExampleViewController {
+ // MARK: Internal
+
+ // In order to reach the subviewInputBar
+ override var inputAccessoryView: UIView? {
+ self.subviewInputBar
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
- private var keyboardManager = KeyboardManager()
+ subviewInputBar.delegate = self
+ // Take into account the height of the bottom input bar
+ additionalBottomInset = 88
+ // Binding to the messagesCollectionView will enable interactive dismissal
+ keyboardManager.bind(to: messagesCollectionView)
+ }
- private let subviewInputBar = InputBarAccessoryView()
+ override func didMove(toParent parent: UIViewController?) {
+ super.didMove(toParent: parent)
+ parent?.view.addSubview(subviewInputBar)
+ // Binding the inputBar will set the needed callback actions to position the inputBar on top of the keyboard
+ keyboardManager.bind(inputAccessoryView: subviewInputBar)
+ }
- override func viewDidLoad() {
- super.viewDidLoad()
- subviewInputBar.delegate = self
- // Take into account the height of the bottom input bar
- additionalBottomInset = 88
- }
+ override func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) {
+ processInputBar(subviewInputBar)
+ }
- override func didMove(toParent parent: UIViewController?) {
- super.didMove(toParent: parent)
- parent?.view.addSubview(subviewInputBar)
- keyboardManager.bind(inputAccessoryView: subviewInputBar)
- }
+ // MARK: Private
- override func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
- processInputBar(subviewInputBar)
- }
+ private let subviewInputBar = InputBarAccessoryView()
}
diff --git a/Example/Sources/View Controllers/NavigationController.swift b/Example/Sources/View Controllers/NavigationController.swift
deleted file mode 100644
index c47cace57..000000000
--- a/Example/Sources/View Controllers/NavigationController.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import UIKit
-
-final class NavigationController: UINavigationController {
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return viewControllers.last?.preferredStatusBarStyle ?? .lightContent
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- navigationBar.isTranslucent = false
- navigationBar.tintColor = .white
- navigationBar.barTintColor = .primaryColor
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
- navigationBar.shadowImage = UIImage()
- navigationBar.setBackgroundImage(UIImage(), for: .default)
- view.backgroundColor = .primaryColor
- }
-
- func setAppearanceStyle(to style: UIStatusBarStyle) {
- if style == .default {
- navigationBar.shadowImage = UIImage()
- navigationBar.barTintColor = .primaryColor
- navigationBar.tintColor = .white
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
- } else if style == .lightContent {
- navigationBar.shadowImage = nil
- navigationBar.barTintColor = .white
- navigationBar.tintColor = UIColor(red: 0, green: 0.5, blue: 1, alpha: 1)
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.black]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.black]
- }
- }
-
-}
diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift
index 137e1f3aa..9c8c5409f 100644
--- a/Example/Sources/View Controllers/SettingsViewController.swift
+++ b/Example/Sources/View Controllers/SettingsViewController.swift
@@ -1,158 +1,185 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MessageKit
+import UIKit
+
+// MARK: - SettingsViewController
final internal class SettingsViewController: UITableViewController {
+ // MARK: Lifecycle
- // MARK: - Properties
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
-
- let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Photo from URL Messages", "Video Messages", "Audio Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages", "ShareContact Messages"]
-
- // MARK: - Picker
-
- var messagesPicker = UIPickerView()
-
- @objc func onDoneWithPickerView() {
- let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0)
- UserDefaults.standard.setMockMessages(count: selectedMessagesCount)
- view.endEditing(false)
- tableView.reloadData()
- }
-
- @objc func dismissPickerView() {
- view.endEditing(false)
- }
-
- private func configurePickerView() {
- messagesPicker.dataSource = self
- messagesPicker.delegate = self
- messagesPicker.backgroundColor = .white
-
- messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false)
- }
-
- // MARK: - Toolbar
-
- var messagesToolbar = UIToolbar()
-
- private func configureToolbar() {
- let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView))
- let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
- let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView))
- messagesToolbar.items = [cancelButton, spaceButton, doneButton]
- messagesToolbar.sizeToFit()
- }
-
- // MARK: - View lifecycle
-
- override func viewDidLoad() {
- super.viewDidLoad()
- tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier)
- tableView.tableFooterView = UIView()
- configurePickerView()
- configureToolbar()
- }
-
- // MARK: - TableViewDelegate & TableViewDataSource
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- return cells.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cellValue = cells[indexPath.row]
- let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()
- cell.textLabel?.text = cells[indexPath.row]
-
- switch cellValue {
- case "Mock messages count":
- return configureTextFieldTableViewCell(at: indexPath)
- default:
- let switchView = UISwitch(frame: .zero)
- switchView.isOn = UserDefaults.standard.bool(forKey: cellValue)
- switchView.tag = indexPath.row
- switchView.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged)
- cell.accessoryView = switchView
- }
- return cell
- }
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- tableView.deselectRow(at: indexPath, animated: true)
-
- let cell = tableView.cellForRow(at: indexPath)
-
- cell?.contentView.subviews.forEach {
- if $0 is UITextField {
- $0.becomeFirstResponder()
- }
- }
+ // MARK: - View lifecycle
+
+ init() {
+ super.init(style: .insetGrouped)
+ }
+
+ required init?(coder _: NSCoder) { nil }
+
+ // MARK: Internal
+
+ // MARK: - Properties
+
+ let cells = [
+ "Mock messages count",
+ "Text Messages",
+ "AttributedText Messages",
+ "Photo Messages",
+ "Photo from URL Messages",
+ "Video Messages",
+ "Audio Messages",
+ "Emoji Messages",
+ "Location Messages",
+ "Url Messages",
+ "Phone Messages",
+ "ShareContact Messages",
+ ]
+
+ var messagesPicker = UIPickerView()
+
+ // MARK: - Toolbar
+
+ var messagesToolbar = UIToolbar()
+
+ @objc
+ func onDoneWithPickerView() {
+ let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0)
+ UserDefaults.standard.setMockMessages(count: selectedMessagesCount)
+ view.endEditing(false)
+ tableView.reloadData()
+ }
+
+ @objc
+ func dismissPickerView() {
+ view.endEditing(false)
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ navigationItem.title = "Settings"
+ tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier)
+ tableView.tableFooterView = UIView()
+ configurePickerView()
+ configureToolbar()
+ }
+
+ // MARK: - TableViewDelegate & TableViewDataSource
+
+ override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
+ cells.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cellValue = cells[indexPath.row]
+ let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()
+ cell.textLabel?.text = cells[indexPath.row]
+
+ switch cellValue {
+ case "Mock messages count":
+ return configureTextFieldTableViewCell(at: indexPath)
+ default:
+ let switchView = UISwitch(frame: .zero)
+ switchView.isOn = UserDefaults.standard.bool(forKey: cellValue)
+ switchView.tag = indexPath.row
+ switchView.onTintColor = .primaryColor
+ switchView.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
+ cell.accessoryView = switchView
}
-
- // MARK: - Helper
-
- private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell {
- if let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as? TextFieldTableViewCell {
- cell.mainLabel.text = "Mock messages count:"
-
- let messagesCount = UserDefaults.standard.mockMessagesCount()
- cell.textField.text = "\(messagesCount)"
-
- cell.textField.inputView = messagesPicker
- cell.textField.inputAccessoryView = messagesToolbar
-
- return cell
- }
- return TextFieldTableViewCell()
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ let cell = tableView.cellForRow(at: indexPath)
+
+ cell?.contentView.subviews.forEach {
+ if $0 is UITextField {
+ $0.becomeFirstResponder()
+ }
}
-
- @objc func switchChanged(_ sender: UISwitch!) {
- let cell = cells[sender.tag]
-
- UserDefaults.standard.set(sender.isOn, forKey: cell)
- UserDefaults.standard.synchronize()
+ }
+
+ @objc
+ func switchChanged(_ sender: UISwitch!) {
+ let cell = cells[sender.tag]
+
+ UserDefaults.standard.set(sender.isOn, forKey: cell)
+ UserDefaults.standard.synchronize()
+ }
+
+ // MARK: Private
+
+ private func configurePickerView() {
+ messagesPicker.dataSource = self
+ messagesPicker.delegate = self
+ messagesPicker.backgroundColor = .white
+
+ messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false)
+ }
+
+ private func configureToolbar() {
+ let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView))
+ let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
+ let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView))
+ messagesToolbar.items = [cancelButton, spaceButton, doneButton]
+ messagesToolbar.sizeToFit()
+ }
+
+ // MARK: - Helper
+
+ private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell {
+ if
+ let cell = tableView.dequeueReusableCell(
+ withIdentifier: TextFieldTableViewCell.identifier,
+ for: indexPath) as? TextFieldTableViewCell
+ {
+ cell.mainLabel.text = "Mock messages count:"
+
+ let messagesCount = UserDefaults.standard.mockMessagesCount()
+ cell.textField.text = "\(messagesCount)"
+
+ cell.textField.inputView = messagesPicker
+ cell.textField.inputAccessoryView = messagesToolbar
+
+ return cell
}
+ return TextFieldTableViewCell()
+ }
}
-// MARK: - UIPickerViewDelegate, UIPickerViewDataSource
+// MARK: UIPickerViewDelegate, UIPickerViewDataSource
+
extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource {
-
- func numberOfComponents(in pickerView: UIPickerView) -> Int {
- return 1
- }
-
- func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
- return 100
- }
-
- func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
- return "\(row)"
- }
+ func numberOfComponents(in _: UIPickerView) -> Int {
+ 1
+ }
+
+ func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int {
+ 100
+ }
+
+ func pickerView(_: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? {
+ "\(row)"
+ }
}
diff --git a/Example/Sources/Views/CameraInputBarAccessoryView.swift b/Example/Sources/Views/CameraInputBarAccessoryView.swift
new file mode 100644
index 000000000..c672d9d95
--- /dev/null
+++ b/Example/Sources/Views/CameraInputBarAccessoryView.swift
@@ -0,0 +1,188 @@
+//
+// CameraInput.swift
+// ChatExample
+//
+// Created by Mohannad on 12/25/20.
+// Copyright © 2020 MessageKit. All rights reserved.
+//
+
+import InputBarAccessoryView
+import UIKit
+
+// MARK: - CameraInputBarAccessoryViewDelegate
+
+protocol CameraInputBarAccessoryViewDelegate: InputBarAccessoryViewDelegate {
+ func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment])
+}
+
+extension CameraInputBarAccessoryViewDelegate {
+ func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: [AttachmentManager.Attachment]) { }
+}
+
+// MARK: - CameraInputBarAccessoryView
+
+class CameraInputBarAccessoryView: InputBarAccessoryView {
+ // MARK: Lifecycle
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ configure()
+ }
+
+ required init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: Internal
+
+ lazy var attachmentManager: AttachmentManager = { [unowned self] in
+ let manager = AttachmentManager()
+ manager.delegate = self
+ return manager
+ }()
+
+ func configure() {
+ let camera = makeButton(named: "ic_camera")
+ camera.tintColor = .darkGray
+ camera.onTouchUpInside { [weak self] _ in
+ self?.showImagePickerControllerActionSheet()
+ }
+ setLeftStackViewWidthConstant(to: 35, animated: true)
+ setStackViewItems([camera], forStack: .left, animated: false)
+ inputPlugins = [attachmentManager]
+ }
+
+ override func didSelectSendButton() {
+ if attachmentManager.attachments.count > 0 {
+ (delegate as? CameraInputBarAccessoryViewDelegate)?
+ .inputBar(self, didPressSendButtonWith: attachmentManager.attachments)
+ }
+ else {
+ delegate?.inputBar(self, didPressSendButtonWith: inputTextView.text)
+ }
+ }
+
+ // MARK: Private
+
+ private func makeButton(named _: String) -> InputBarButtonItem {
+ InputBarButtonItem()
+ .configure {
+ $0.spacing = .fixed(10)
+ $0.image = UIImage(systemName: "camera.fill")?.withRenderingMode(.alwaysTemplate)
+ $0.setSize(CGSize(width: 30, height: 30), animated: false)
+ }.onSelected {
+ $0.tintColor = .systemBlue
+ }.onDeselected {
+ $0.tintColor = UIColor.lightGray
+ }.onTouchUpInside { _ in
+ print("Item Tapped")
+ }
+ }
+}
+
+// MARK: UIImagePickerControllerDelegate, UINavigationControllerDelegate
+
+extension CameraInputBarAccessoryView: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+ @objc
+ func showImagePickerControllerActionSheet() {
+ let photoLibraryAction = UIAlertAction(title: "Choose From Library", style: .default) { [weak self] _ in
+ self?.showImagePickerController(sourceType: .photoLibrary)
+ }
+
+ let cameraAction = UIAlertAction(title: "Take From Camera", style: .default) { [weak self] _ in
+ self?.showImagePickerController(sourceType: .camera)
+ }
+
+ let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
+
+ AlertService.showAlert(
+ style: .actionSheet,
+ title: "Choose Your Image",
+ message: nil,
+ actions: [photoLibraryAction, cameraAction, cancelAction],
+ completion: nil)
+ }
+
+ func showImagePickerController(sourceType: UIImagePickerController.SourceType) {
+ let imgPicker = UIImagePickerController()
+ imgPicker.delegate = self
+ imgPicker.allowsEditing = true
+ imgPicker.sourceType = sourceType
+ imgPicker.presentationController?.delegate = self
+ inputAccessoryView?.isHidden = true
+ getRootViewController()?.present(imgPicker, animated: true, completion: nil)
+ }
+
+ func imagePickerController(
+ _: UIImagePickerController,
+ didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any])
+ {
+ if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
+ // self.sendImageMessage(photo: editedImage)
+ inputPlugins.forEach { _ = $0.handleInput(of: editedImage) }
+ }
+ else if let originImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
+ inputPlugins.forEach { _ = $0.handleInput(of: originImage) }
+ // self.sendImageMessage(photo: originImage)
+ }
+ getRootViewController()?.dismiss(animated: true, completion: nil)
+ inputAccessoryView?.isHidden = false
+ }
+
+ func imagePickerControllerDidCancel(_: UIImagePickerController) {
+ getRootViewController()?.dismiss(animated: true, completion: nil)
+ inputAccessoryView?.isHidden = false
+ }
+
+ func getRootViewController() -> UIViewController? {
+ (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController
+ }
+}
+
+// MARK: AttachmentManagerDelegate
+
+extension CameraInputBarAccessoryView: AttachmentManagerDelegate {
+ // MARK: - AttachmentManagerDelegate
+
+ func attachmentManager(_: AttachmentManager, shouldBecomeVisible: Bool) {
+ setAttachmentManager(active: shouldBecomeVisible)
+ }
+
+ func attachmentManager(_ manager: AttachmentManager, didReloadTo _: [AttachmentManager.Attachment]) {
+ sendButton.isEnabled = manager.attachments.count > 0
+ }
+
+ func attachmentManager(_ manager: AttachmentManager, didInsert _: AttachmentManager.Attachment, at _: Int) {
+ sendButton.isEnabled = manager.attachments.count > 0
+ }
+
+ func attachmentManager(_ manager: AttachmentManager, didRemove _: AttachmentManager.Attachment, at _: Int) {
+ sendButton.isEnabled = manager.attachments.count > 0
+ }
+
+ func attachmentManager(_: AttachmentManager, didSelectAddAttachmentAt _: Int) {
+ showImagePickerControllerActionSheet()
+ }
+
+ // MARK: - AttachmentManagerDelegate Helper
+
+ func setAttachmentManager(active: Bool) {
+ let topStackView = topStackView
+ if active, !topStackView.arrangedSubviews.contains(attachmentManager.attachmentView) {
+ topStackView.insertArrangedSubview(attachmentManager.attachmentView, at: topStackView.arrangedSubviews.count)
+ topStackView.layoutIfNeeded()
+ } else if !active, topStackView.arrangedSubviews.contains(attachmentManager.attachmentView) {
+ topStackView.removeArrangedSubview(attachmentManager.attachmentView)
+ topStackView.layoutIfNeeded()
+ }
+ }
+}
+
+// MARK: UIAdaptivePresentationControllerDelegate
+
+extension CameraInputBarAccessoryView: UIAdaptivePresentationControllerDelegate {
+ // Swipe to dismiss image modal
+ public func presentationControllerWillDismiss(_: UIPresentationController) {
+ isHidden = false
+ }
+}
diff --git a/Example/Sources/Views/CustomCell.swift b/Example/Sources/Views/CustomCell.swift
index a229f344b..55330ddb4 100644
--- a/Example/Sources/Views/CustomCell.swift
+++ b/Example/Sources/Views/CustomCell.swift
@@ -1,64 +1,66 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MessageKit
+import UIKit
open class CustomCell: UICollectionViewCell {
-
- let label = UILabel()
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupSubviews()
- }
-
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupSubviews()
- }
-
- open func setupSubviews() {
- contentView.addSubview(label)
- label.textAlignment = .center
- label.font = UIFont.italicSystemFont(ofSize: 13)
- }
-
- open override func layoutSubviews() {
- super.layoutSubviews()
- label.frame = contentView.bounds
- }
-
- open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- // Do stuff
- switch message.kind {
- case .custom(let data):
- guard let systemMessage = data as? String else { return }
- label.text = systemMessage
- default:
- break
- }
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupSubviews()
+ }
+
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ open func setupSubviews() {
+ contentView.addSubview(label)
+ label.textAlignment = .center
+ label.font = UIFont.italicSystemFont(ofSize: 13)
+ }
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ label.frame = contentView.bounds
+ }
+
+ open func configure(with message: MessageType, at _: IndexPath, and _: MessagesCollectionView) {
+ // Do stuff
+ switch message.kind {
+ case .custom(let data):
+ guard let systemMessage = data as? String else { return }
+ label.text = systemMessage
+ default:
+ break
}
-
+ }
+
+ // MARK: Internal
+
+ let label = UILabel()
}
diff --git a/Example/Sources/Views/CustomMessageContentCell.swift b/Example/Sources/Views/CustomMessageContentCell.swift
new file mode 100644
index 000000000..7d64e1468
--- /dev/null
+++ b/Example/Sources/Views/CustomMessageContentCell.swift
@@ -0,0 +1,132 @@
+//
+// CustomMessageContentCell.swift
+// ChatExample
+//
+// Created by Vignesh J on 01/05/21.
+// Copyright © 2021 MessageKit. All rights reserved.
+//
+
+import MessageKit
+import UIKit
+
+class CustomMessageContentCell: MessageCollectionViewCell {
+ // MARK: Lifecycle
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ // MARK: Internal
+
+ /// The `MessageCellDelegate` for the cell.
+ weak var delegate: MessageCellDelegate?
+
+ /// The container used for styling and holding the message's content view.
+ var messageContainerView: UIView = {
+ let containerView = UIView()
+ containerView.clipsToBounds = true
+ containerView.layer.masksToBounds = true
+ return containerView
+ }()
+
+ /// The top label of the cell.
+ var cellTopLabel: UILabel = {
+ let label = UILabel()
+ label.numberOfLines = 0
+ label.textAlignment = .center
+ return label
+ }()
+
+ var cellDateLabel: UILabel = {
+ let label = UILabel()
+ label.numberOfLines = 0
+ label.textAlignment = .right
+ return label
+ }()
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ cellTopLabel.text = nil
+ cellTopLabel.attributedText = nil
+ cellDateLabel.text = nil
+ cellDateLabel.attributedText = nil
+ }
+
+ /// Handle tap gesture on contentView and its subviews.
+ override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: self)
+
+ switch true {
+ case messageContainerView.frame
+ .contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
+ delegate?.didTapMessage(in: self)
+ case cellTopLabel.frame.contains(touchLocation):
+ delegate?.didTapCellTopLabel(in: self)
+ case cellDateLabel.frame.contains(touchLocation):
+ delegate?.didTapMessageBottomLabel(in: self)
+ default:
+ delegate?.didTapBackground(in: self)
+ }
+ }
+
+ /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
+ override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ let touchPoint = gestureRecognizer.location(in: self)
+ guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
+ return messageContainerView.frame.contains(touchPoint)
+ }
+
+ func setupSubviews() {
+ messageContainerView.layer.cornerRadius = 5
+
+ contentView.addSubview(cellTopLabel)
+ contentView.addSubview(messageContainerView)
+ messageContainerView.addSubview(cellDateLabel)
+ }
+
+ func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView,
+ dataSource: MessagesDataSource,
+ and sizeCalculator: CustomLayoutSizeCalculator)
+ {
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ return
+ }
+ cellTopLabel.frame = sizeCalculator.cellTopLabelFrame(
+ for: message,
+ at: indexPath)
+ cellDateLabel.frame = sizeCalculator.cellMessageBottomLabelFrame(
+ for: message,
+ at: indexPath)
+ messageContainerView.frame = sizeCalculator.messageContainerFrame(
+ for: message,
+ at: indexPath,
+ fromCurrentSender: dataSource
+ .isFromCurrentSender(message: message))
+ cellTopLabel.attributedText = dataSource.cellTopLabelAttributedText(
+ for: message,
+ at: indexPath)
+ cellDateLabel.attributedText = dataSource.messageBottomLabelAttributedText(
+ for: message,
+ at: indexPath)
+ messageContainerView.backgroundColor = displayDelegate.backgroundColor(
+ for: message,
+ at: indexPath,
+ in: messagesCollectionView)
+ }
+
+ /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
+ func cellContentView(canHandle _: CGPoint) -> Bool {
+ false
+ }
+}
diff --git a/Example/Sources/Views/CustomTextMessageContentCell.swift b/Example/Sources/Views/CustomTextMessageContentCell.swift
new file mode 100644
index 000000000..f4e2eb0fb
--- /dev/null
+++ b/Example/Sources/Views/CustomTextMessageContentCell.swift
@@ -0,0 +1,70 @@
+//
+// CustomTextMessageContentCell.swift
+// ChatExample
+//
+// Created by Vignesh J on 01/05/21.
+// Copyright © 2021 MessageKit. All rights reserved.
+//
+
+import MessageKit
+import UIKit
+
+class CustomTextMessageContentCell: CustomMessageContentCell {
+ /// The label used to display the message's text.
+ var messageLabel: UILabel = {
+ let label = UILabel()
+ label.numberOfLines = 0
+ label.font = UIFont.preferredFont(forTextStyle: .body)
+
+ return label
+ }()
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+
+ messageLabel.attributedText = nil
+ messageLabel.text = nil
+ }
+
+ override func setupSubviews() {
+ super.setupSubviews()
+
+ messageContainerView.addSubview(messageLabel)
+ }
+
+ override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView,
+ dataSource: MessagesDataSource,
+ and sizeCalculator: CustomLayoutSizeCalculator)
+ {
+ super.configure(
+ with: message,
+ at: indexPath,
+ in: messagesCollectionView,
+ dataSource: dataSource,
+ and: sizeCalculator)
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ return
+ }
+
+ let calculator = sizeCalculator as? CustomTextLayoutSizeCalculator
+ messageLabel.frame = calculator?.messageLabelFrame(
+ for: message,
+ at: indexPath) ?? .zero
+
+ let textMessageKind = message.kind
+ switch textMessageKind {
+ case .text(let text), .emoji(let text):
+ let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
+ messageLabel.text = text
+ messageLabel.textColor = textColor
+ case .attributedText(let text):
+ messageLabel.attributedText = text
+ default:
+ break
+ }
+ }
+}
diff --git a/Example/Sources/Views/SwiftUI/MessagesView.swift b/Example/Sources/Views/SwiftUI/MessagesView.swift
index ce4a7fb9e..c6f6fcbae 100644
--- a/Example/Sources/Views/SwiftUI/MessagesView.swift
+++ b/Example/Sources/Views/SwiftUI/MessagesView.swift
@@ -1,147 +1,163 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import SwiftUI
-import MessageKit
import InputBarAccessoryView
+import MessageKit
+import SwiftUI
+
+// MARK: - MessageSwiftUIVC
final class MessageSwiftUIVC: MessagesViewController {
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- // Because SwiftUI wont automatically make our controller the first responder, we need to do it on viewDidAppear
- becomeFirstResponder()
- messagesCollectionView.scrollToLastItem(animated: true)
- }
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ // Because SwiftUI wont automatically make our controller the first responder, we need to do it on viewDidAppear
+ becomeFirstResponder()
+ messagesCollectionView.scrollToLastItem(animated: true)
+ }
}
-@available(iOS 13.0, *)
+// MARK: - MessagesView
+
struct MessagesView: UIViewControllerRepresentable {
-
- @State var initialized = false
- @Binding var messages: [MessageType]
-
- func makeUIViewController(context: Context) -> MessagesViewController {
- let messagesVC = MessageSwiftUIVC()
-
- messagesVC.messagesCollectionView.messagesDisplayDelegate = context.coordinator
- messagesVC.messagesCollectionView.messagesLayoutDelegate = context.coordinator
- messagesVC.messagesCollectionView.messagesDataSource = context.coordinator
- messagesVC.messageInputBar.delegate = context.coordinator
- messagesVC.scrollsToLastItemOnKeyboardBeginsEditing = true // default false
- messagesVC.maintainPositionOnKeyboardFrameChanged = true // default false
- messagesVC.showMessageTimestampOnSwipeLeft = true // default false
-
- return messagesVC
- }
-
- func updateUIViewController(_ uiViewController: MessagesViewController, context: Context) {
- uiViewController.messagesCollectionView.reloadData()
- scrollToBottom(uiViewController)
- }
-
- private func scrollToBottom(_ uiViewController: MessagesViewController) {
- DispatchQueue.main.async {
- // The initialized state variable allows us to start at the bottom with the initial messages without seeing the inital scroll flash by
- uiViewController.messagesCollectionView.scrollToLastItem(animated: self.initialized)
- self.initialized = true
- }
- }
-
- func makeCoordinator() -> Coordinator {
- return Coordinator(messages: $messages)
- }
-
- final class Coordinator {
-
- let formatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .medium
- return formatter
- }()
-
- var messages: Binding<[MessageType]>
- init(messages: Binding<[MessageType]>) {
- self.messages = messages
- }
-
+ // MARK: Internal
+
+ final class Coordinator {
+ // MARK: Lifecycle
+
+ init(messages: Binding<[MessageType]>) {
+ self.messages = messages
}
+ // MARK: Internal
+
+ let formatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ return formatter
+ }()
+
+ var messages: Binding<[MessageType]>
+ }
+
+ @State var initialized = false
+ @Binding var messages: [MessageType]
+
+ func makeUIViewController(context: Context) -> MessagesViewController {
+ let messagesVC = MessageSwiftUIVC()
+
+ messagesVC.messagesCollectionView.messagesDisplayDelegate = context.coordinator
+ messagesVC.messagesCollectionView.messagesLayoutDelegate = context.coordinator
+ messagesVC.messagesCollectionView.messagesDataSource = context.coordinator
+ messagesVC.messageInputBar.delegate = context.coordinator
+ messagesVC.scrollsToLastItemOnKeyboardBeginsEditing = true // default false
+ messagesVC.maintainPositionOnInputBarHeightChanged = true // default false
+ messagesVC.showMessageTimestampOnSwipeLeft = true // default false
+
+ return messagesVC
+ }
+
+ func updateUIViewController(_ uiViewController: MessagesViewController, context _: Context) {
+ uiViewController.messagesCollectionView.reloadData()
+ scrollToBottom(uiViewController)
+ }
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(messages: $messages)
+ }
+
+ // MARK: Private
+
+ private func scrollToBottom(_ uiViewController: MessagesViewController) {
+ DispatchQueue.main.async {
+ // The initialized state variable allows us to start at the bottom with the initial messages without seeing the initial scroll flash by
+ uiViewController.messagesCollectionView.scrollToLastItem(animated: self.initialized)
+ self.initialized = true
+ }
+ }
}
-@available(iOS 13.0, *)
+// MARK: - MessagesView.Coordinator + MessagesDataSource
+
extension MessagesView.Coordinator: MessagesDataSource {
- func currentSender() -> SenderType {
- return SampleData.shared.currentSender
- }
-
- func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
- return messages.wrappedValue[indexPath.section]
- }
-
- func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
- return messages.wrappedValue.count
- }
-
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let name = message.sender.displayName
- return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
-
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let dateString = formatter.string(from: message.sentDate)
- return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
- }
+ var currentSender: SenderType {
+ SampleData.shared.currentSender
+ }
- func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let sentDate = message.sentDate
- let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate)
- let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10)
- let timeLabelColor: UIColor = .systemGray
- return NSAttributedString(string: sentDateString, attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor])
- }
+ func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
+ messages.wrappedValue[indexPath.section]
+ }
+
+ func numberOfSections(in _: MessagesCollectionView) -> Int {
+ messages.wrappedValue.count
+ }
+
+ func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let name = message.sender.displayName
+ return NSAttributedString(
+ string: name,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
+ }
+
+ func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let dateString = formatter.string(from: message.sentDate)
+ return NSAttributedString(
+ string: dateString,
+ attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
+ }
+
+ func messageTimestampLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let sentDate = message.sentDate
+ let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate)
+ let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10)
+ let timeLabelColor: UIColor = .systemGray
+ return NSAttributedString(
+ string: sentDateString,
+ attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor])
+ }
}
-@available(iOS 13.0, *)
+// MARK: - MessagesView.Coordinator + InputBarAccessoryViewDelegate
+
extension MessagesView.Coordinator: InputBarAccessoryViewDelegate {
- func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
- let message = MockMessage(text: text, user: SampleData.shared.currentSender, messageId: UUID().uuidString, date: Date())
- messages.wrappedValue.append(message)
- inputBar.inputTextView.text = ""
- }
+ func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
+ let message = MockMessage(text: text, user: SampleData.shared.currentSender, messageId: UUID().uuidString, date: Date())
+ messages.wrappedValue.append(message)
+ inputBar.inputTextView.text = ""
+ }
}
-@available(iOS 13.0, *)
+// MARK: - MessagesView.Coordinator + MessagesLayoutDelegate, MessagesDisplayDelegate
+
extension MessagesView.Coordinator: MessagesLayoutDelegate, MessagesDisplayDelegate {
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
- avatarView.set(avatar: avatar)
- }
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 20
- }
-
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 16
- }
+ func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
+ avatarView.set(avatar: avatar)
+ }
+
+ func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 20
+ }
+
+ func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 16
+ }
}
diff --git a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift
index 41d3b0f26..b4fd02bc0 100644
--- a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift
+++ b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift
@@ -6,38 +6,53 @@
// Copyright © 2020 MessageKit. All rights reserved.
//
-import SwiftUI
import MessageKit
+import SwiftUI
+
+// MARK: - SwiftUIExampleView
-@available(iOS 13.0, *)
struct SwiftUIExampleView: View {
-
- @State var messages: [MessageType] = SampleData.shared.getMessages(count: 20)
-
- var body: some View {
- MessagesView(messages: $messages).onAppear {
- self.connectToMessageSocket()
- }.onDisappear {
- self.cleanupSocket()
- }
- .navigationBarTitle("SwiftUI Example", displayMode: .inline)
- }
-
- private func connectToMessageSocket() {
- MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]).onNewMessage { message in
- self.messages.append(message)
- }
+ // MARK: Internal
+
+ @State var messages: [MessageType] = SampleData.shared.getMessages(count: 20)
+
+ var body: some View {
+ MessagesView(messages: $messages).onAppear {
+ self.connectToMessageSocket()
+ }.onDisappear {
+ self.cleanupSocket()
}
+ .navigationBarTitle("SwiftUI Example", displayMode: .inline)
+ .modifier(IgnoresSafeArea()) //fixes issue with IBAV placement when keyboard appears
+ }
+
+ // MARK: Private
- private func cleanupSocket() {
- MockSocket.shared.disconnect()
+ 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
+ self.messages.append(message)
}
-
+ }
+
+ private func cleanupSocket() {
+ MockSocket.shared.disconnect()
+ }
}
-@available(iOS 13.0, *)
+// MARK: - SwiftUIExampleView_Previews
+
struct SwiftUIExampleView_Previews: PreviewProvider {
- static var previews: some View {
- SwiftUIExampleView()
- }
+ static var previews: some View {
+ SwiftUIExampleView()
+ }
}
diff --git a/Example/Sources/Views/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift
index c03081d81..fb4cc5d0a 100644
--- a/Example/Sources/Views/TableViewCells.swift
+++ b/Example/Sources/Views/TableViewCells.swift
@@ -1,62 +1,63 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
internal class TextFieldTableViewCell: UITableViewCell {
+ // MARK: Lifecycle
- static let identifier = "TextFieldTableViewCellIdentifier"
-
- var mainLabel = UILabel()
- var textField = UITextField()
-
- // MARK: - View lifecycle
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- mainLabel.translatesAutoresizingMaskIntoConstraints = false
- textField.translatesAutoresizingMaskIntoConstraints = false
-
- contentView.addSubview(mainLabel)
- contentView.addSubview(textField)
-
- NSLayoutConstraint.activate([
- mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
- mainLabel.widthAnchor.constraint(equalToConstant: 200),
- mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
-
- textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
-
- textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
- textField.widthAnchor.constraint(equalToConstant: 50)
- ])
-
- textField.textAlignment = .right
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
+ // MARK: - View lifecycle
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ mainLabel.translatesAutoresizingMaskIntoConstraints = false
+ textField.translatesAutoresizingMaskIntoConstraints = false
+
+ contentView.addSubview(mainLabel)
+ contentView.addSubview(textField)
+
+ NSLayoutConstraint.activate([
+ mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
+ mainLabel.widthAnchor.constraint(equalToConstant: 200),
+ mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+
+ textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+
+ textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
+ textField.widthAnchor.constraint(equalToConstant: 50),
+ ])
+
+ textField.textAlignment = .right
+ }
+
+ required init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: Internal
+
+ static let identifier = "TextFieldTableViewCellIdentifier"
+
+ var mainLabel = UILabel()
+ var textField = UITextField()
}
diff --git a/Example/Tests/ChatExampleTests.swift b/Example/Tests/ChatExampleTests.swift
index f29a4705f..6e376e447 100644
--- a/Example/Tests/ChatExampleTests.swift
+++ b/Example/Tests/ChatExampleTests.swift
@@ -20,18 +20,16 @@ import XCTest
@testable import ChatExample
final class ChatExampleTests: XCTestCase {
+ override func setUp() {
+ super.setUp()
+ }
- override func setUp() {
- super.setUp()
- }
-
- override func tearDown() {
- super.tearDown()
- }
-
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct results.
- }
+ override func tearDown() {
+ super.tearDown()
+ }
+ func testExample() {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ }
}
diff --git a/Example/UITests/ChatExampleUITests.swift b/Example/UITests/ChatExampleUITests.swift
index df4df7276..8c810b170 100644
--- a/Example/UITests/ChatExampleUITests.swift
+++ b/Example/UITests/ChatExampleUITests.swift
@@ -20,27 +20,25 @@ import XCTest
@testable import ChatExample
final class ChatExampleUITests: XCTestCase {
+ override func setUp() {
+ super.setUp()
- override func setUp() {
- super.setUp()
+ // In UI tests it is usually best to stop immediately when a failure occurs.
+ continueAfterFailure = false
+ // UI tests must launch the application that they test.
+ // Doing this in setup will make sure it happens for each test method.
+ XCUIApplication().launch()
- // In UI tests it is usually best to stop immediately when a failure occurs.
- continueAfterFailure = false
- // UI tests must launch the application that they test.
- // Doing this in setup will make sure it happens for each test method.
- XCUIApplication().launch()
-
- // In UI tests it’s important to set the initial state
- // - such as interface orientation - required for your tests before they run.
- // The setUp method is a good place to do this.
- }
-
- func testExampleRuns() {
- // Extremely simple UI test which is designed to run and display the example project
- // This should show if there are any very obvious crashes on render
- let app = XCUIApplication()
- app.tables.staticTexts["Test"].tap()
- XCTAssertTrue(app.collectionViews.staticTexts["Check out this awesome UI library for Chat"].exists)
- }
+ // In UI tests it’s important to set the initial state
+ // - such as interface orientation - required for your tests before they run.
+ // The setUp method is a good place to do this.
+ }
+ func testExampleRuns() {
+ // Extremely simple UI test which is designed to run and display the example project
+ // This should show if there are any very obvious crashes on render
+ let app = XCUIApplication()
+ app.tables.staticTexts["Test"].tap()
+ XCTAssertTrue(app.collectionViews.staticTexts["Check out this awesome UI library for Chat"].exists)
+ }
}
diff --git a/Gemfile b/Gemfile
index f2e566d0c..a8a0b8f14 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,5 +22,5 @@
# SOFTWARE.
source 'https://rubygems.org'
-gem 'danger', '~> 6.2'
-gem 'danger-swiftlint', '~> 0.24'
+gem 'danger', '~> 8.6'
+gem 'danger-swiftlint', '~> 0.29'
diff --git a/Gemfile.lock b/Gemfile.lock
index 62e89c6a6..57adcd5ab 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,9 @@
GEM
remote: https://rubygems.org/
specs:
- addressable (2.7.0)
- public_suffix (>= 2.0.2, < 5.0)
- claide (1.0.3)
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
+ claide (1.1.0)
claide-plugins (0.9.2)
cork
nap
@@ -11,58 +11,81 @@ GEM
colored2 (3.1.2)
cork (0.3.0)
colored2 (~> 3.1)
- danger (6.3.2)
+ danger (8.6.1)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored2 (~> 3.1)
cork (~> 0.1)
- faraday (~> 0.9)
+ faraday (>= 0.9.0, < 2.0)
faraday-http-cache (~> 2.0)
- git (~> 1.6)
- kramdown (~> 2.0)
+ git (~> 1.7)
+ kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0)
no_proxy_fix
octokit (~> 4.7)
- terminal-table (~> 1)
- danger-swiftlint (0.24.5)
+ terminal-table (>= 1, < 4)
+ danger-swiftlint (0.30.2)
danger
rake (> 10)
thor (~> 0.19)
- faraday (0.17.4)
- multipart-post (>= 1.2, < 3)
- faraday-http-cache (2.2.0)
+ faraday (1.10.2)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-http-cache (2.4.1)
faraday (>= 0.8)
- git (1.8.1)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (1.0.1)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ git (1.13.1)
+ addressable (~> 2.8)
rchardet (~> 1.8)
- kramdown (2.3.1)
+ kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
- multipart-post (2.1.1)
+ multipart-post (2.2.3)
nap (1.1.0)
no_proxy_fix (0.1.2)
- octokit (4.20.0)
- faraday (>= 0.9)
- sawyer (~> 0.8.0, >= 0.5.3)
+ octokit (4.25.1)
+ faraday (>= 1, < 3)
+ sawyer (~> 0.9)
open4 (1.3.4)
- public_suffix (4.0.6)
- rake (13.0.3)
+ public_suffix (5.0.1)
+ rake (13.0.6)
rchardet (1.8.0)
- rexml (3.2.4)
- sawyer (0.8.2)
+ rexml (3.3.9)
+ ruby2_keywords (0.0.5)
+ sawyer (0.9.2)
addressable (>= 2.3.5)
- faraday (> 0.8, < 2.0)
- terminal-table (1.8.0)
- unicode-display_width (~> 1.1, >= 1.1.1)
+ faraday (>= 0.17.3, < 3)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
thor (0.20.3)
- unicode-display_width (1.7.0)
+ unicode-display_width (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
- danger (~> 6.2)
- danger-swiftlint (~> 0.24)
+ danger (~> 8.6)
+ danger-swiftlint (~> 0.29)
BUNDLED WITH
2.1.4
diff --git a/GitHubActions/build.sh b/GitHubActions/build.sh
deleted file mode 100755
index a040ab0fd..000000000
--- a/GitHubActions/build.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/bash
-
-#
-# MIT License
-#
-# Copyright (c) 2017-2020 MessageKit
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-set -e
-function trap_handler {
- echo -e "\n\nOh no! You walked directly into the slavering fangs of a lurking grue!"
- echo "**** You have died ****"
- exit 255
-}
-trap trap_handler INT TERM EXIT
-
-MODE="$1"
-
-if [ "$MODE" = "tests" -o "$MODE" = "all" ]; then
- echo "Running MessageKit tests."
- set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 11" | xcpretty -c
- success="1"
-fi
-
-if [ "$MODE" = "framework" -o "$MODE" = "all" ]; then
- echo "Building MessageKit Framework."
- set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 11" | xcpretty -c
- success="1"
-fi
-
-if [ "$MODE" = "example" -o "$MODE" = "all" ]; then
- echo "Building & testing MessageKit Example app."
- cd Example
- set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 11" CODE_SIGNING_REQUIRED=NO | xcpretty -c
- success="1"
-fi
-
-if [ "$success" = "1" ]; then
-trap - EXIT
-exit 0
-fi
-
-echo "Unrecognised mode '$MODE'."
diff --git a/LICENSE.md b/LICENSE.md
index 20cc288d5..4731930ad 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017-2020 MessageKit
+Copyright (c) 2017-2024 MessageKit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..56e05c78f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+# MIT License
+#
+# Copyright (c) 2017-2022 MessageKit
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+.SHELLFLAGS = -ec
+.SHELL = /bin/bash
+
+test:
+ @echo "Running MessageKit tests."
+ @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16" | xcpretty -c
+
+framework:
+ @echo "Building MessageKit Framework."
+ @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 16" | 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 16" CODE_SIGNING_REQUIRED=NO | xcpretty -c
+
+format:
+ @swift package --allow-writing-to-package-directory format-source-code --file .
+
+lint:
+ @swift package --disable-sandbox lint
+
+setup:
+ @mkdir -p .git/hooks
+ @rm -f .git/hooks/pre-commit
+ @cp ./Scripts/pre-commit ./.git/hooks
+ @chmod +x .git/hooks/pre-commit
diff --git a/MessageKit.podspec b/MessageKit.podspec
deleted file mode 100644
index 35fdfe813..000000000
--- a/MessageKit.podspec
+++ /dev/null
@@ -1,21 +0,0 @@
-Pod::Spec.new do |s|
- s.name = 'MessageKit'
- s.version = '3.5.1'
- s.license = { :type => "MIT", :file => "LICENSE.md" }
-
- s.summary = 'An elegant messages UI library for iOS.'
- s.homepage = 'https://github.com/MessageKit/MessageKit'
- s.social_media_url = 'https://twitter.com/_SD10_'
- s.author = { "Steven Deutsch" => "stevensdeutsch@yahoo.com" }
-
- s.source = { :git => 'https://github.com/MessageKit/MessageKit.git', :tag => s.version }
- s.source_files = 'Sources/**/*.swift'
-
- s.swift_version = '5.3'
-
- s.ios.deployment_target = '12.0'
- s.ios.resource_bundle = { 'MessageKit' => 'Sources/Assets.xcassets' }
-
- s.dependency 'InputBarAccessoryView', '~> 5.3.0'
-
-end
diff --git a/Package.swift b/Package.swift
index eaf578e2c..ca0bcbd50 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,40 +1,39 @@
-// swift-tools-version:5.3
-// The swift-tools-version declares the minimum version of Swift required to build this package.
+// swift-tools-version:6.0
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import PackageDescription
let package = Package(
name: "MessageKit",
- platforms: [.iOS(.v12)],
+ platforms: [.iOS(.v14)],
products: [
- .library(name: "MessageKit", targets: ["MessageKit"]),
+ .library(name: "MessageKit", targets: ["MessageKit"])
],
dependencies: [
- .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "5.3.0"))
+ .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "7.0.0")),
],
targets: [
+ // MARK: - MessageKit
+
.target(
name: "MessageKit",
dependencies: ["InputBarAccessoryView"],
@@ -42,7 +41,7 @@ let package = Package(
exclude: ["Supporting/Info.plist", "Supporting/MessageKit.h"],
swiftSettings: [SwiftSetting.define("IS_SPM")]
),
- .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"])
+ .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]),
],
- swiftLanguageVersions: [.v5]
+ swiftLanguageModes: [.v6]
)
diff --git a/README.md b/README.md
index a9109f5ba..260d48555 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,25 @@
-
-
- A community-driven replacement for JSQMessagesViewController https://messagekit.github.io
+
-
-[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Tests%22)
-[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Build+Framework%22)
-[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22PR+Example+app%22)
-[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Danger%22)
-
-[](https://codecov.io/gh/MessageKit/MessageKit)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ A community-driven replacement for JSQMessagesViewController
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -38,34 +32,9 @@
- Provide an awesome Open Source project for the iOS open source community.
- Help others learn.
-## Vision
-
-See [VISION.md](https://github.com/MessageKit/MessageKit/blob/master/VISION.md) for Goals, Scope, & Technical Considerations.
-
## Installation
-### [CocoaPods](https://cocoapods.org/) **Recommended**
-
-```ruby
-# Swift 5.3
-pod 'MessageKit'
-```
-
-> For Swift 5.0 use version 3.3.0
-
-```ruby
-# Swift 5.0
-pod 'MessageKit', '~> 3.3.0'
-```
-
-> For Swift 4.2 use version 3.0.0
-
-```ruby
-# Swift 4.2
-pod 'MessageKit', '~> 3.0.0'
-```
-
-### [Swift Package Manager](https://swift.org/package-manager/)
+### [Swift Package Manager](https://swift.org/package-manager/) - **Recommended**
Swift 5.3 in Xcode 12 [added support](https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md) for assets in Swift Packages.
You can [just add](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) MessageKit package to your project by entering it's repository URL
@@ -73,21 +42,33 @@ You can [just add](https://developer.apple.com/documentation/xcode/adding_packag
```
https://github.com/MessageKit/MessageKit
```
-Older versions of Swift and XCode don't support MessageKit via SPM.
+Older versions of Swift and Xcode don't support MessageKit via SPM.
### [Manual](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md)
## Requirements
-- **iOS 12** or later
-- **Swift 5.3** or later
+- **iOS 14** or later
+- **Swift 6** or later
+
+> For iOS 13 or Swift 5.x please use version 4.3.0
+
+> For iOS 12 or CocoaPods please use version 3.8.0
> For iOS 11 please use version 3.3.0
-> For iOS 9 and iOS 10 please use version 3.1.0
+> For iOS 9 and iOS 10 please use version 3.1.1
## Getting Started
+Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md).
+
+We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHub with the **"Question"** tag.
+
+For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md)
+
+Check out the full documentation [here](https://messagekit.github.io/MessageKit/documentation/messagekit).
+
### Cell Structure
@@ -98,21 +79,13 @@ Each default cell is a subclass of [`MessageContentCell`](https://github.com/Mes
This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project.
-### MessageInputBar Structure
+### InputBarAccessoryView Structure
-The `MessageInputBar`, derrived from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. It is self-sizing which means as the user types it will grow to fill available space. It is centered around the `middleContentView` which by default holds the `InputTextView`. This is surrounded by `InputStackView`'s that will also grow in high based on the needs of their subviews `intrinsicContentSize`. See the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project for examples on how to taylor the layout for your own needs.
-
-### Guides
-
-Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md).
-
-We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHub with the **"Question"** tag.
-
-For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md)
+The `InputBarAccessoryView`, 3rd party dependency from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. Check the repo and examples there for more info.
## Default Cells
@@ -150,11 +123,16 @@ If you choose to use the `.custom` kind you are responsible for all of the cells
## Contributing
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Tests%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Build+Framework%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22PR+Example+app%22)
+[](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Danger%22)
+
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.
@@ -177,6 +155,9 @@ 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)
- [HopUp](https://itunes.apple.com/us/app/hopup-airsoft-community/id1128903141?mt=8)
@@ -191,7 +172,12 @@ Add your app to the list of apps using this library and make a pull request.
- [Charge Running](https://apps.apple.com/app/charge-running-live-coaching/id1204578360)
- [HER](https://apps.apple.com/us/app/id573328837)
- [Girlfriend Plus](https://apps.apple.com/us/app/girlfriend-plus/id1011637655)
-
+- [Noon Happen](https://apps.apple.com/app/id1477310602)
+- [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._
## Core Team
diff --git a/Scripts/pre-commit b/Scripts/pre-commit
new file mode 100644
index 000000000..ad5572c4d
--- /dev/null
+++ b/Scripts/pre-commit
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+git diff --diff-filter=d --staged --name-only | grep -e '\(.*\).swift$' | while read line; do
+ swift package --allow-writing-to-package-directory format-source-code --file "${line}";
+ git add "$line";
+done
\ No newline at end of file
diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift
index 59e0926e1..974fa1077 100644
--- a/Sources/Controllers/MessagesViewController+Keyboard.swift
+++ b/Sources/Controllers/MessagesViewController+Keyboard.swift
@@ -1,144 +1,152 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
import Foundation
-import UIKit
import InputBarAccessoryView
+import UIKit
-internal extension MessagesViewController {
-
- // MARK: - Register / Unregister Observers
-
- 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)
- }
-
- 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)
- }
-
- // MARK: - Notification Handlers
-
- @objc
- private func handleTextViewDidBeginEditing(_ notification: Notification) {
- if scrollsToLastItemOnKeyboardBeginsEditing || scrollsToLastItemOnKeyboardBeginsEditing {
- guard
- let inputTextView = notification.object as? InputTextView,
- inputTextView === messageInputBar.inputTextView
- else {
- return
- }
- if scrollsToLastItemOnKeyboardBeginsEditing {
- messagesCollectionView.scrollToLastItem()
- } else {
- messagesCollectionView.scrollToLastItem(animated: true)
- }
- }
- }
-
- @objc
- private func handleKeyboardDidChangeState(_ notification: Notification) {
- guard !isMessagesControllerBeingDismissed else { return }
-
- guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return }
- guard !keyboardStartFrameInScreenCoords.isEmpty || UIDevice.current.userInterfaceIdiom != .pad else {
- // WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change
- // notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to
- // 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
- // change notifications while the keyboard is hidden or undocked (undocked keyboard is considered hidden by those events).
- // Unfortunately, we do care about the difference between hidden and undocked, because we have an input bar which is at the
- // bottom when the keyboard is hidden, and is tied to the keyboard when it's undocked.
- //
- // If we follow what Apple recommends and ignore notifications while the keyboard is hidden/undocked, we get an extra inset
- // at the bottom when the undocked keyboard is visible (the inset that tries to compensate for the missing input bar).
- // (Alternatives like setting newBottomInset to 0 or to the height of the input bar don't work either.)
- //
- // 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
-
- UIView.performWithoutAnimation {
- messageCollectionViewBottomInset = newBottomInset
- }
-
- if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
- let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
- // Changing contentOffset to bigger number than the contentSize will result in a jump of content
- // https://github.com/MessageKit/MessageKit/issues/1486
- guard contentOffset.y <= messagesCollectionView.contentSize.height else {
- return
- }
- messagesCollectionView.setContentOffset(contentOffset, animated: false)
- }
- }
-
- // MARK: - Inset Computation
-
- private func requiredScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat {
- // 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.
- return max(0, additionalBottomInset - automaticallyAddedBottomInset)
- } else {
- return max(0, intersection.height + additionalBottomInset - automaticallyAddedBottomInset)
+extension MessagesViewController {
+ // MARK: Internal
+
+ // MARK: - Register Observers
+
+ internal func addKeyboardObservers() {
+ 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
+ NotificationCenter.default
+ .publisher(for: UITextView.textDidBeginEditingNotification)
+ .subscribe(on: DispatchQueue.global())
+ /// Wait for inputBar frame change animation to end
+ .delay(for: .milliseconds(200), scheduler: DispatchQueue.main)
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] notification in
+ self?.handleTextViewDidBeginEditing(notification)
+ }
+ .store(in: &disposeBag)
+
+ NotificationCenter.default
+ .publisher(for: UITextView.textDidChangeNotification)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .compactMap { $0.object as? InputTextView }
+ .filter { [weak self] textView in
+ textView == self?.messageInputBar.inputTextView
+ }
+ .map(\.text)
+ .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)
+
+ 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)
+ .removeDuplicates()
+ .sink(receiveValue: { [weak self] _ in
+ self?.updateMessageCollectionViewBottomInset()
+ })
+ .store(in: &disposeBag)
+ }
+
+ // MARK: - Updating insets
+
+ /// Updates bottom messagesCollectionView inset based on the position of inputContainerView
+ internal func updateMessageCollectionViewBottomInset() {
+ let collectionViewHeight = messagesCollectionView.frame.maxY
+ let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) -
+ automaticallyAddedBottomInset
+ let normalizedNewBottomInset = max(0, newBottomInset)
+ let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
+
+ UIView.performWithoutAnimation {
+ guard differenceOfBottomInset != 0 else { return }
+ messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
+ messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
}
-
- func requiredInitialScrollViewBottomInset() -> CGFloat {
- let inputAccessoryViewHeight = inputAccessoryView?.frame.height ?? 0
- return max(0, inputAccessoryViewHeight + additionalBottomInset - automaticallyAddedBottomInset)
- }
-
- /// UIScrollView can automatically add safe area insets to its contentInset,
- /// which needs to be accounted for when setting the contentInset based on screen coordinates.
- ///
- /// - Returns: The distance automatically added to contentInset.bottom, if any.
- private var automaticallyAddedBottomInset: CGFloat {
- return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom
+ }
+
+ // MARK: Private
+
+ /// UIScrollView can automatically add safe area insets to its contentInset,
+ /// which needs to be accounted for when setting the contentInset based on screen coordinates.
+ ///
+ /// - Returns: The distance automatically added to contentInset.bottom, if any.
+ private var automaticallyAddedBottomInset: CGFloat {
+ messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset
+ }
+
+ private var messageCollectionViewBottomInset: CGFloat {
+ messagesCollectionView.contentInset.bottom
+ }
+
+ /// UIScrollView can automatically add safe area insets to its contentInset,
+ /// which needs to be accounted for when setting the contentInset based on screen coordinates.
+ ///
+ /// - Returns: The distance automatically added to contentInset.top, if any.
+ private var automaticallyAddedTopInset: CGFloat {
+ messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
+ }
+
+ private var messageCollectionViewTopInset: CGFloat {
+ messagesCollectionView.contentInset.top
+ }
+
+ // MARK: - Private methods
+
+ private func handleTextViewDidBeginEditing(_ notification: Notification) {
+ guard
+ scrollsToLastItemOnKeyboardBeginsEditing,
+ let inputTextView = notification.object as? InputTextView,
+ inputTextView === messageInputBar.inputTextView
+ else {
+ return
}
+ messagesCollectionView.scrollToLastItem()
+ }
}
diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift
index 67e5b8cc4..7aa07a36d 100644
--- a/Sources/Controllers/MessagesViewController+Menu.swift
+++ b/Sources/Controllers/MessagesViewController+Menu.swift
@@ -1,100 +1,113 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-internal extension MessagesViewController {
+extension MessagesViewController {
+ // MARK: Internal
- // MARK: - Register / Unregister Observers
+ // MARK: - Register / Unregister Observers
- func addMenuControllerObservers() {
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil)
- }
-
- func removeMenuControllerObservers() {
- NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
- }
-
- // MARK: - Notification Handlers
-
- /// Show menuController and set target rect to selected bubble
- @objc
- private func menuControllerWillShow(_ notification: Notification) {
-
- guard let currentMenuController = notification.object as? UIMenuController,
- let selectedIndexPath = selectedIndexPathForMenu else { return }
-
- NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
- defer {
- NotificationCenter.default.addObserver(self,
- selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
- name: UIMenuController.willShowMenuNotification, object: nil)
- selectedIndexPathForMenu = nil
- }
-
- currentMenuController.setMenuVisible(false, animated: false)
+ internal func addMenuControllerObservers() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
+ name: UIMenuController.willShowMenuNotification,
+ object: nil)
+ }
- guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { return }
- let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view)
+ // MARK: Private
- var messageInputBarFrame: CGRect = .zero
- if let messageInputBarSuperview = messageInputBar.superview {
- messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview)
- }
+ // MARK: - Helpers
- var topNavigationBarFrame: CGRect = navigationBarFrame
- if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview {
- topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview)
- }
-
- let menuHeight = currentMenuController.menuFrame.height
-
- let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight)
+ private var navigationBarFrame: CGRect {
+ guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else {
+ return .zero
+ }
+ return navigationController.navigationBar.frame
+ }
+
+ // MARK: - Notification Handlers
+
+ /// Show menuController and set target rect to selected bubble
+ @objc
+ private func menuControllerWillShow(_ notification: Notification) {
+ guard
+ let currentMenuController = notification.object as? UIMenuController,
+ let selectedIndexPath = selectedIndexPathForMenu else { return }
+
+ NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
+ defer {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
+ name: UIMenuController.willShowMenuNotification,
+ object: nil)
+ selectedIndexPathForMenu = nil
+ }
- var targetRect: CGRect = selectedCellMessageBubbleFrame
- currentMenuController.arrowDirection = .default
+ currentMenuController.hideMenu()
- /// Message bubble intersects with navigationBar and keyboard
- if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) {
- let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2
- targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1)
- } /// Message bubble only intersects with navigationBar
- else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) {
- currentMenuController.arrowDirection = .up
- }
+ guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else {
+ return
+ }
+ let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view)
- currentMenuController.setTargetRect(targetRect, in: view)
- currentMenuController.setMenuVisible(true, animated: true)
+ var messageInputBarFrame: CGRect = .zero
+ if let messageInputBarSuperview = messageInputBar.superview {
+ messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview)
}
- // MARK: - Helpers
+ var topNavigationBarFrame: CGRect = navigationBarFrame
+ if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview {
+ topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview)
+ }
- private var navigationBarFrame: CGRect {
- guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else {
- return .zero
- }
- return navigationController.navigationBar.frame
+ let menuHeight = currentMenuController.menuFrame.height
+
+ let selectedCellMessageBubblePlusMenuFrame = CGRect(
+ selectedCellMessageBubbleFrame.origin.x,
+ selectedCellMessageBubbleFrame.origin.y - menuHeight,
+ selectedCellMessageBubbleFrame.size.width,
+ selectedCellMessageBubbleFrame.size.height + 2 * menuHeight)
+
+ var targetRect: CGRect = selectedCellMessageBubbleFrame
+ currentMenuController.arrowDirection = .default
+
+ /// Message bubble intersects with navigationBar and keyboard
+ if
+ selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame),
+ selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame)
+ {
+ let centerY = (
+ selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame)
+ .minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2
+ targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1)
+ } /// Message bubble only intersects with navigationBar
+ else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) {
+ currentMenuController.arrowDirection = .up
}
+
+ currentMenuController.showMenu(from: view, rect: targetRect)
+ }
}
diff --git a/Sources/Controllers/MessagesViewController+PanGesture.swift b/Sources/Controllers/MessagesViewController+PanGesture.swift
new file mode 100644
index 000000000..dc81391d7
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+PanGesture.swift
@@ -0,0 +1,97 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesViewController {
+ // MARK: Internal
+
+ /// Display time of message by swiping the cell
+ func addPanGesture() {
+ panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
+ guard let panGesture = panGesture else {
+ return
+ }
+ panGesture.delegate = self
+ messagesCollectionView.addGestureRecognizer(panGesture)
+ messagesCollectionView.clipsToBounds = false
+ }
+
+ func removePanGesture() {
+ guard let panGesture = panGesture else {
+ return
+ }
+ panGesture.delegate = nil
+ self.panGesture = nil
+ messagesCollectionView.removeGestureRecognizer(panGesture)
+ messagesCollectionView.clipsToBounds = true
+ }
+
+ // MARK: Private
+
+ @objc
+ private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
+ guard let parentView = gesture.view else {
+ return
+ }
+
+ switch gesture.state {
+ case .began, .changed:
+ messagesCollectionView.showsVerticalScrollIndicator = false
+ let translation = gesture.translation(in: view)
+ let minX = -(view.frame.size.width * 0.35)
+ let maxX: CGFloat = 0
+ var offsetValue = translation.x
+ offsetValue = max(offsetValue, minX)
+ offsetValue = min(offsetValue, maxX)
+ parentView.frame.origin.x = offsetValue
+ case .ended:
+ messagesCollectionView.showsVerticalScrollIndicator = true
+ UIView.animate(
+ withDuration: 0.5,
+ delay: 0,
+ usingSpringWithDamping: 0.7,
+ initialSpringVelocity: 0.8,
+ options: .curveEaseOut,
+ animations: {
+ parentView.frame.origin.x = 0
+ },
+ completion: nil)
+ default:
+ break
+ }
+ }
+}
+
+// MARK: - MessagesViewController + UIGestureRecognizerDelegate
+
+extension MessagesViewController: UIGestureRecognizerDelegate {
+ /// Check Pan Gesture Direction:
+ open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
+ return false
+ }
+ let velocity = panGesture.velocity(in: messagesCollectionView)
+ return abs(velocity.x) > abs(velocity.y)
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+State.swift b/Sources/Controllers/MessagesViewController+State.swift
new file mode 100644
index 000000000..62d2d9cdb
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+State.swift
@@ -0,0 +1,100 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
+import Foundation
+import InputBarAccessoryView
+import UIKit
+
+extension MessagesViewController {
+ @MainActor
+ final class State {
+ /// Pan gesture for display the date of message by swiping left.
+ var panGesture: UIPanGestureRecognizer?
+ var maintainPositionOnInputBarHeightChanged = false
+ var scrollsToLastItemOnKeyboardBeginsEditing = false
+
+ let inputContainerView: MessagesInputContainerView = .init()
+ @Published var inputBarType: MessageInputBarKind = .messageInputBar
+ let keyboardManager = KeyboardManager()
+ var disposeBag: Set = .init()
+ }
+
+ // MARK: - Getters
+
+ public var keyboardManager: KeyboardManager { state.keyboardManager }
+
+ var panGesture: UIPanGestureRecognizer? {
+ get { state.panGesture }
+ set { state.panGesture = newValue }
+ }
+
+ var disposeBag: Set {
+ get { state.disposeBag }
+ set { state.disposeBag = newValue }
+ }
+}
+
+extension MessagesViewController {
+ /// Container holding `messageInputBar` view. To change type of input bar, set `inputBarType` to desired kind.
+ public var inputContainerView: MessagesInputContainerView { state.inputContainerView }
+
+ /// Kind of `messageInputBar` to be added into `inputContainerView`
+ public var inputBarType: MessageInputBarKind {
+ get { state.inputBarType }
+ set { state.inputBarType = newValue }
+ }
+
+ /// A Boolean value that determines whether the `MessagesCollectionView`
+ /// maintains it's current position when the height of the `MessageInputBar` changes.
+ ///
+ /// The default value of this property is `false`.
+ @available(
+ *,
+ deprecated,
+ renamed: "maintainPositionOnInputBarHeightChanged",
+ message: "Please use new property - maintainPositionOnInputBarHeightChanged")
+ public var maintainPositionOnKeyboardFrameChanged: Bool {
+ get { state.maintainPositionOnInputBarHeightChanged }
+ set { state.maintainPositionOnInputBarHeightChanged = newValue }
+ }
+
+ /// A Boolean value that determines whether the `MessagesCollectionView`
+ /// maintains it's current position when the height of the `MessageInputBar` changes.
+ ///
+ /// The default value of this property is `false` and the `MessagesCollectionView` will scroll to bottom after the
+ /// height of the `MessageInputBar` changes.
+ public var maintainPositionOnInputBarHeightChanged: Bool {
+ get { state.maintainPositionOnInputBarHeightChanged }
+ set { state.maintainPositionOnInputBarHeightChanged = newValue }
+ }
+
+ /// 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
+ public var scrollsToLastItemOnKeyboardBeginsEditing: Bool {
+ get { state.scrollsToLastItemOnKeyboardBeginsEditing }
+ set { state.scrollsToLastItemOnKeyboardBeginsEditing = newValue }
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+TypingIndicator.swift b/Sources/Controllers/MessagesViewController+TypingIndicator.swift
new file mode 100644
index 000000000..434a66bb2
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+TypingIndicator.swift
@@ -0,0 +1,96 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+// MARK: - Typing Indicator API
+
+extension MessagesViewController {
+ // MARK: Open
+
+ /// Sets the typing indicator sate by inserting/deleting the `TypingBubbleCell`
+ ///
+ /// - Parameters:
+ /// - isHidden: A Boolean value that is to be the new state of the typing indicator
+ /// - animated: A Boolean value determining if the insertion is to be animated
+ /// - updates: A block of code that will be executed during `performBatchUpdates`
+ /// when `animated` is `TRUE` or before the `completion` block executes
+ /// when `animated` is `FALSE`
+ /// - completion: A completion block to execute after the insertion/deletion
+ @objc
+ open func setTypingIndicatorViewHidden(
+ _ isHidden: Bool,
+ animated: Bool,
+ whilePerforming updates: (() -> Void)? = nil,
+ completion: ((Bool) -> Void)? = nil)
+ {
+ guard isTypingIndicatorHidden != isHidden else {
+ completion?(false)
+ return
+ }
+
+ let section = messagesCollectionView.numberOfSections
+
+ if animated {
+ messagesCollectionView.performBatchUpdates({ [weak self] in
+ self?.messagesCollectionView.setTypingIndicatorViewHidden(isHidden)
+ self?.performUpdatesForTypingIndicatorVisability(at: section)
+ updates?()
+ }, completion: completion)
+ } else {
+ messagesCollectionView.setTypingIndicatorViewHidden(isHidden)
+ performUpdatesForTypingIndicatorVisability(at: section)
+ updates?()
+ completion?(true)
+ }
+ }
+
+ // MARK: Public
+
+ public var isTypingIndicatorHidden: Bool {
+ messagesCollectionView.isTypingIndicatorHidden
+ }
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ !messagesCollectionView.isTypingIndicatorHidden && section == numberOfSections(in: messagesCollectionView) - 1
+ }
+
+ // MARK: Private
+
+ /// Performs a delete or insert on the `MessagesCollectionView` on the provided section
+ ///
+ /// - Parameter section: The index to modify
+ private func performUpdatesForTypingIndicatorVisability(at section: Int) {
+ if isTypingIndicatorHidden {
+ messagesCollectionView.deleteSections([section - 1])
+ } else {
+ messagesCollectionView.insertSections([section])
+ }
+ }
+}
diff --git a/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift
new file mode 100644
index 000000000..e524215b4
--- /dev/null
+++ b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift
@@ -0,0 +1,39 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesViewController: UIScrollViewDelegate {
+ open func scrollViewDidScroll(_: UIScrollView) { }
+ open func scrollViewWillBeginDragging(_: UIScrollView) { }
+ open func scrollViewWillEndDragging(
+ _: UIScrollView,
+ withVelocity _: CGPoint,
+ targetContentOffset _: UnsafeMutablePointer) { }
+ open func scrollViewDidEndDragging(_: UIScrollView, willDecelerate _: Bool) { }
+ open func scrollViewWillBeginDecelerating(_: UIScrollView) { }
+ open func scrollViewDidEndDecelerating(_: UIScrollView) { }
+ open func scrollViewDidEndScrollingAnimation(_: UIScrollView) { }
+ open func scrollViewDidScrollToTop(_: UIScrollView) { }
+ open func scrollViewDidChangeAdjustedContentInset(_: UIScrollView) { }
+}
diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift
index f9c4d5c5c..41965d5a4 100644
--- a/Sources/Controllers/MessagesViewController.swift
+++ b/Sources/Controllers/MessagesViewController.swift
@@ -1,508 +1,403 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Combine
import Foundation
-import UIKit
import InputBarAccessoryView
+import UIKit
/// A subclass of `UIViewController` with a `MessagesCollectionView` object
/// that is used to display conversation interfaces.
-open class MessagesViewController: UIViewController,
-UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIGestureRecognizerDelegate {
-
- /// The `MessagesCollectionView` managed by the messages view controller object.
- open var messagesCollectionView = MessagesCollectionView()
-
- /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
- 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
- @available(*, deprecated, message: "Control scrolling to bottom on keyboardBeginEditing by using scrollsToLastItemOnKeyboardBeginsEditing instead", renamed: "scrollsToLastItemOnKeyboardBeginsEditing")
- open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false
-
- /// A Boolean value that determines whether the `MessagesCollectionView`
- /// maintains it's current position when the height of the `MessageInputBar` changes.
- ///
- /// The default value of this property is `false`.
- open var maintainPositionOnKeyboardFrameChanged: Bool = false
-
- /// Display the date of message by swiping left.
- /// The default value of this property is `false`.
- open var showMessageTimestampOnSwipeLeft: Bool = false {
- didSet {
- messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft
- if showMessageTimestampOnSwipeLeft {
- addPanGesture()
- } else {
- removePanGesture()
- }
- }
- }
-
- /// Pan gesture for display the date of message by swiping left.
- private var panGesture: UIPanGestureRecognizer?
-
- open override var canBecomeFirstResponder: Bool {
- return true
- }
-
- open override var inputAccessoryView: UIView? {
- return messageInputBar
- }
-
- open override var shouldAutorotate: Bool {
- return false
- }
-
- /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically
- /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used
- /// as a measure of last resort when the built-in algorithm does not produce the right
- /// value for your app. Please let us know when you end up having to use this property.
- open var additionalBottomInset: CGFloat = 0 {
- didSet {
- let delta = additionalBottomInset - oldValue
- messageCollectionViewBottomInset += delta
- }
- }
-
- public var isTypingIndicatorHidden: Bool {
- return messagesCollectionView.isTypingIndicatorHidden
- }
-
- public var selectedIndexPathForMenu: IndexPath?
-
- private var isFirstLayout: Bool = true
-
- internal var isMessagesControllerBeingDismissed: Bool = false
-
- internal var messageCollectionViewBottomInset: CGFloat = 0 {
- didSet {
- messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset
- messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset
- }
- }
-
- // MARK: - View Life Cycle
-
- open override func viewDidLoad() {
- super.viewDidLoad()
- setupDefaults()
- setupSubviews()
- setupConstraints()
- setupDelegates()
- addMenuControllerObservers()
- addObservers()
- }
-
- open override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- if !isFirstLayout {
- addKeyboardObservers()
- }
- }
-
- open override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- isMessagesControllerBeingDismissed = false
- }
-
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- isMessagesControllerBeingDismissed = true
- removeKeyboardObservers()
- }
-
- open override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- isMessagesControllerBeingDismissed = false
- }
-
- open override func viewDidLayoutSubviews() {
- // Hack to prevent animation of the contentInset after viewDidAppear
- if isFirstLayout {
- defer { isFirstLayout = false }
- addKeyboardObservers()
- messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset()
- }
- }
-
- open override func viewSafeAreaInsetsDidChange() {
- super.viewSafeAreaInsetsDidChange()
- messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset()
- }
-
- // MARK: - Initializers
-
- deinit {
- removeMenuControllerObservers()
- removeObservers()
- clearMemoryCache()
- }
-
- // MARK: - Methods [Private]
-
- /// Display time of message by swiping the cell
- private func addPanGesture() {
- panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
- guard let panGesture = panGesture else {
- return
- }
- panGesture.delegate = self
- messagesCollectionView.addGestureRecognizer(panGesture)
- messagesCollectionView.clipsToBounds = false
- }
-
- private func removePanGesture() {
- guard let panGesture = panGesture else {
- return
- }
- panGesture.delegate = nil
- self.panGesture = nil
- messagesCollectionView.removeGestureRecognizer(panGesture)
- messagesCollectionView.clipsToBounds = true
- }
-
- @objc
- private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
- guard let parentView = gesture.view else {
- return
- }
-
- switch gesture.state {
- case .began, .changed:
- messagesCollectionView.showsVerticalScrollIndicator = false
- let translation = gesture.translation(in: view)
- let minX = -(view.frame.size.width * 0.35)
- let maxX: CGFloat = 0
- var offsetValue = translation.x
- offsetValue = max(offsetValue, minX)
- offsetValue = min(offsetValue, maxX)
- parentView.frame.origin.x = offsetValue
- case .ended:
- messagesCollectionView.showsVerticalScrollIndicator = true
- UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: .curveEaseOut, animations: {
- parentView.frame.origin.x = 0
- }, completion: nil)
- default:
- break
- }
- }
-
- private func setupDefaults() {
- extendedLayoutIncludesOpaqueBars = true
- view.backgroundColor = .collectionViewBackground
- messagesCollectionView.keyboardDismissMode = .interactive
- messagesCollectionView.alwaysBounceVertical = true
- messagesCollectionView.backgroundColor = .collectionViewBackground
- if #available(iOS 13.0, *) {
- messagesCollectionView.automaticallyAdjustsScrollIndicatorInsets = false
- }
- }
-
- private func setupDelegates() {
- messagesCollectionView.delegate = self
- messagesCollectionView.dataSource = self
- }
-
- private func setupSubviews() {
- view.addSubview(messagesCollectionView)
- }
-
- private func setupConstraints() {
- messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false
-
- let top = messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor)
- let bottom = messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor)
- let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
- NSLayoutConstraint.activate([top, bottom, trailing, leading])
- }
-
- // MARK: - Typing Indicator API
-
- /// Sets the typing indicator sate by inserting/deleting the `TypingBubbleCell`
- ///
- /// - Parameters:
- /// - isHidden: A Boolean value that is to be the new state of the typing indicator
- /// - animated: A Boolean value determining if the insertion is to be animated
- /// - updates: A block of code that will be executed during `performBatchUpdates`
- /// when `animated` is `TRUE` or before the `completion` block executes
- /// when `animated` is `FALSE`
- /// - completion: A completion block to execute after the insertion/deletion
- open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) {
-
- guard isTypingIndicatorHidden != isHidden else {
- completion?(false)
- return
- }
-
- let section = messagesCollectionView.numberOfSections
- messagesCollectionView.setTypingIndicatorViewHidden(isHidden)
-
- if animated {
- messagesCollectionView.performBatchUpdates({ [weak self] in
- self?.performUpdatesForTypingIndicatorVisability(at: section)
- updates?()
- }, completion: completion)
- } else {
- performUpdatesForTypingIndicatorVisability(at: section)
- updates?()
- completion?(true)
- }
- }
-
- /// Performs a delete or insert on the `MessagesCollectionView` on the provided section
- ///
- /// - Parameter section: The index to modify
- private func performUpdatesForTypingIndicatorVisability(at section: Int) {
- if isTypingIndicatorHidden {
- messagesCollectionView.deleteSections([section - 1])
- } else {
- messagesCollectionView.insertSections([section])
- }
- }
-
- /// A method that by default checks if the section is the last in the
- /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
- /// is FALSE
- ///
- /// - Parameter section
- /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
- public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
- return !messagesCollectionView.isTypingIndicatorHidden && section == self.numberOfSections(in: messagesCollectionView) - 1
- }
-
- // MARK: - UICollectionViewDataSource
-
- open func numberOfSections(in collectionView: UICollectionView) -> Int {
- guard let collectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- let sections = collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0
- return collectionView.isTypingIndicatorHidden ? sections : sections + 1
- }
-
- open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- guard let collectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- if isSectionReservedForTypingIndicator(section) {
- return 1
- }
- return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0
- }
-
- /// Notes:
- /// - If you override this method, remember to call MessagesDataSource's customCell(for:at:in:)
- /// for MessageKind.custom messages, if necessary.
- ///
- /// - If you are using the typing indicator you will need to ensure that the section is not
- /// reserved for it with `isSectionReservedForTypingIndicator` defined in
- /// `MessagesCollectionViewFlowLayout`
- open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
-
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
-
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
-
- if isSectionReservedForTypingIndicator(indexPath.section) {
- return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView)
- }
-
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text, .attributedText, .emoji:
- let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .photo, .video:
- let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .location:
- let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .audio:
- let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .contact:
- let cell = messagesCollectionView.dequeueReusableCell(ContactMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .linkPreview:
- let cell = messagesCollectionView.dequeueReusableCell(LinkPreviewMessageCell.self, for: indexPath)
- cell.configure(with: message, at: indexPath, and: messagesCollectionView)
- return cell
- case .custom:
- return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView)
- }
- }
-
- open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
-
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
-
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
-
- switch kind {
- case UICollectionView.elementKindSectionHeader:
- return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView)
- case UICollectionView.elementKindSectionFooter:
- return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView)
- default:
- fatalError(MessageKitError.unrecognizedSectionKind)
- }
- }
-
- // MARK: - UICollectionViewDelegateFlowLayout
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
- guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero }
- return messagesFlowLayout.sizeForItem(at: indexPath)
- }
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
-
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- if isSectionReservedForTypingIndicator(section) {
- return .zero
- }
- return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView)
- }
-
- open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
- guard let cell = cell as? TypingIndicatorCell else { return }
- cell.typingBubble.startAnimating()
- }
-
- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.notMessagesCollectionView)
- }
- guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- if isSectionReservedForTypingIndicator(section) {
- return .zero
- }
- return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView)
- }
-
- open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false }
-
- if isSectionReservedForTypingIndicator(indexPath.section) {
- return false
- }
-
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text, .attributedText, .emoji, .photo:
- selectedIndexPathForMenu = indexPath
- return true
- default:
- return false
- }
- }
-
- open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
- if isSectionReservedForTypingIndicator(indexPath.section) {
- return false
- }
- return (action == NSSelectorFromString("copy:"))
- }
-
- open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- let pasteBoard = UIPasteboard.general
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
-
- switch message.kind {
- case .text(let text), .emoji(let text):
- pasteBoard.string = text
- case .attributedText(let attributedText):
- pasteBoard.string = attributedText.string
- case .photo(let mediaItem):
- pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage
- default:
- break
- }
- }
-
- // MARK: - Helpers
-
- private func addObservers() {
- NotificationCenter.default.addObserver(
- self, selector: #selector(clearMemoryCache), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
- }
-
- private func removeObservers() {
- NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
- }
-
- @objc private func clearMemoryCache() {
- MessageStyle.bubbleImageCache.removeAllObjects()
- }
-
- // MARK: - UIGestureRecognizerDelegate
-
- /// check pan gesture direction
- public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
- return false
- }
- let velocity = panGesture.velocity(in: messagesCollectionView)
- return abs(velocity.x) > abs(velocity.y)
- }
+open class MessagesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
+ // MARK: Lifecycle
+
+ deinit {
+ NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil)
+ MessageStyle.bubbleImageCache.removeAllObjects()
+ }
+
+ // MARK: Open
+
+ /// The `MessagesCollectionView` managed by the messages view controller object.
+ open var messagesCollectionView = MessagesCollectionView()
+
+ /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
+ open lazy var messageInputBar = InputBarAccessoryView()
+
+ /// Display the date of message by swiping left.
+ /// The default value of this property is `false`.
+ open var showMessageTimestampOnSwipeLeft = false {
+ didSet {
+ messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft
+ if showMessageTimestampOnSwipeLeft {
+ addPanGesture()
+ } else {
+ removePanGesture()
+ }
+ }
+ }
+
+ /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically
+ /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used
+ /// as a measure of last resort when the built-in algorithm does not produce the right
+ /// value for your app. Please let us know when you end up having to use this property.
+ open var additionalBottomInset: CGFloat = 0 {
+ didSet {
+ updateMessageCollectionViewBottomInset()
+ }
+ }
+
+ /// withAdditionalBottomSpace parameter for InputBarAccessoryView's KeyboardManager
+ open func inputBarAdditionalBottomSpace() -> CGFloat {
+ 0
+ }
+
+ open override func viewDidLoad() {
+ super.viewDidLoad()
+ setupDefaults()
+ setupSubviews()
+ setupConstraints()
+ setupInputBar(for: inputBarType)
+ setupDelegates()
+ addObservers()
+ addKeyboardObservers()
+ addMenuControllerObservers()
+ /// Layout input container view and update messagesCollectionViewInsets
+ view.layoutIfNeeded()
+ }
+
+ open override func viewSafeAreaInsetsDidChange() {
+ super.viewSafeAreaInsetsDidChange()
+ updateMessageCollectionViewBottomInset()
+ }
+
+ // MARK: - UICollectionViewDataSource
+
+ open func numberOfSections(in collectionView: UICollectionView) -> Int {
+ guard let collectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+ let sections = collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0
+ return collectionView.isTypingIndicatorHidden ? sections : sections + 1
+ }
+
+ open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ guard let collectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+ if isSectionReservedForTypingIndicator(section) {
+ return 1
+ }
+ return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0
+ }
+
+ /// Notes:
+ /// - If you override this method, remember to call MessagesDataSource's customCell(for:at:in:)
+ /// for MessageKind.custom messages, if necessary.
+ ///
+ /// - If you are using the typing indicator you will need to ensure that the section is not
+ /// reserved for it with `isSectionReservedForTypingIndicator` defined in
+ /// `MessagesCollectionViewFlowLayout`
+ open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
+
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView)
+ }
+
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+
+ switch message.kind {
+ case .text, .attributedText, .emoji:
+ if let cell = messagesDataSource.textCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .photo, .video:
+ if let cell = messagesDataSource.photoCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .location:
+ if let cell = messagesDataSource.locationCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .audio:
+ if let cell = messagesDataSource.audioCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .contact:
+ if let cell = messagesDataSource.contactCell(for: message, at: indexPath, in: messagesCollectionView) {
+ return cell
+ } else {
+ let cell = messagesCollectionView.dequeueReusableCell(ContactMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ }
+ case .linkPreview:
+ let cell = messagesCollectionView.dequeueReusableCell(LinkPreviewMessageCell.self, for: indexPath)
+ cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+ return cell
+ case .custom:
+ return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView)
+ }
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ viewForSupplementaryElementOfKind kind: String,
+ at indexPath: IndexPath)
+ -> UICollectionReusableView
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ }
+
+ switch kind {
+ case UICollectionView.elementKindSectionHeader:
+ return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView)
+ case UICollectionView.elementKindSectionFooter:
+ return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView)
+ default:
+ fatalError(MessageKitError.unrecognizedSectionKind)
+ }
+ }
+
+ // MARK: - UICollectionViewDelegateFlowLayout
+
+ open func collectionView(
+ _: UICollectionView,
+ layout collectionViewLayout: UICollectionViewLayout,
+ sizeForItemAt indexPath: IndexPath)
+ -> CGSize
+ {
+ guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero }
+ return messagesFlowLayout.sizeForItem(at: indexPath)
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ layout _: UICollectionViewLayout,
+ referenceSizeForHeaderInSection section: Int)
+ -> CGSize
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+ guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
+ }
+ if isSectionReservedForTypingIndicator(section) {
+ return .zero
+ }
+ return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView)
+ }
+
+ open func collectionView(_: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt _: IndexPath) {
+ guard let cell = cell as? TypingIndicatorCell else { return }
+ cell.typingBubble.startAnimating()
+ }
+
+ open func collectionView(
+ _ collectionView: UICollectionView,
+ layout _: UICollectionViewLayout,
+ referenceSizeForFooterInSection section: Int)
+ -> CGSize
+ {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.notMessagesCollectionView)
+ }
+ guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
+ }
+ if isSectionReservedForTypingIndicator(section) {
+ return .zero
+ }
+ return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView)
+ }
+
+ open func collectionView(_: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false }
+
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return false
+ }
+
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+
+ switch message.kind {
+ case .text, .attributedText, .emoji, .photo:
+ selectedIndexPathForMenu = indexPath
+ return true
+ default:
+ return false
+ }
+ }
+
+ open func collectionView(
+ _: UICollectionView,
+ canPerformAction action: Selector,
+ forItemAt indexPath: IndexPath,
+ withSender _: Any?)
+ -> Bool
+ {
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return false
+ }
+ return (action == NSSelectorFromString("copy:"))
+ }
+
+ open func collectionView(_: UICollectionView, performAction _: Selector, forItemAt indexPath: IndexPath, withSender _: Any?) {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
+ let pasteBoard = UIPasteboard.general
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+
+ switch message.kind {
+ case .text(let text), .emoji(let text):
+ pasteBoard.string = text
+ case .attributedText(let attributedText):
+ pasteBoard.string = attributedText.string
+ case .photo(let mediaItem):
+ pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage
+ default:
+ break
+ }
+ }
+
+ // MARK: Public
+
+ public var selectedIndexPathForMenu: IndexPath?
+
+ // MARK: Internal
+
+ // MARK: - Internal properties
+
+ internal let state: State = .init()
+
+ // MARK: Private
+
+ // MARK: - Private methods
+
+ private func setupDefaults() {
+ extendedLayoutIncludesOpaqueBars = true
+ view.backgroundColor = .collectionViewBackground
+ messagesCollectionView.keyboardDismissMode = .interactive
+ messagesCollectionView.alwaysBounceVertical = true
+ messagesCollectionView.backgroundColor = .collectionViewBackground
+ }
+
+ private func setupSubviews() {
+ view.addSubviews(messagesCollectionView, inputContainerView)
+ }
+
+ private func setupConstraints() {
+ messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false
+ /// Constraints of inputContainerView are managed by keyboardManager
+ inputContainerView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor),
+ messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ ])
+ }
+
+ private func setupDelegates() {
+ messagesCollectionView.delegate = self
+ messagesCollectionView.dataSource = self
+ }
+
+ private func setupInputBar(for kind: MessageInputBarKind) {
+ inputContainerView.subviews.forEach { $0.removeFromSuperview() }
+
+ func pinViewToInputContainer(_ view: UIView) {
+ view.translatesAutoresizingMaskIntoConstraints = false
+ inputContainerView.addSubviews(view)
+
+ NSLayoutConstraint.activate([
+ view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
+ view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
+ view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
+ ])
+ }
+
+ switch kind {
+ case .messageInputBar:
+ pinViewToInputContainer(messageInputBar)
+ case .custom(let view):
+ pinViewToInputContainer(view)
+ }
+ }
+
+ private func addObservers() {
+ NotificationCenter.default
+ .publisher(for: UIApplication.didReceiveMemoryWarningNotification)
+ .subscribe(on: DispatchQueue.global())
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ self?.clearMemoryCache()
+ }
+ .store(in: &disposeBag)
+
+ state.$inputBarType
+ .subscribe(on: DispatchQueue.global())
+ .dropFirst()
+ .removeDuplicates()
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] newType in
+ self?.setupInputBar(for: newType)
+ })
+ .store(in: &disposeBag)
+ }
+
+ private func clearMemoryCache() {
+ MessageStyle.bubbleImageCache.removeAllObjects()
+ }
}
diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift
index 4fe47c26a..aa4ca7d4f 100644
--- a/Sources/Extensions/Bundle+Extensions.swift
+++ b/Sources/Extensions/Bundle+Extensions.swift
@@ -1,40 +1,33 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-internal extension Bundle {
- #if IS_SPM
- static var messageKitAssetBundle: Bundle = Bundle.module
- #else
- static var messageKitAssetBundle: Bundle {
- guard let url = Bundle(for: MessagesViewController.self).url(forResource: "MessageKit", withExtension: "bundle"),
- let resourcesBundle = Bundle(url: url)
- else {
- fatalError(MessageKitError.couldNotLoadAssetsBundle)
- }
- return resourcesBundle
- }
- #endif
+extension Bundle {
+ #if IS_SPM
+ nonisolated internal static let messageKitAssetBundle = Bundle.module
+ #else
+ nonisolated internal static var messageKitAssetBundle: Bundle {
+ Bundle(for: MessagesViewController.self)
+ }
+ #endif
}
diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift
index 5efc13426..2ead04b23 100644
--- a/Sources/Extensions/CGRect+Extensions.swift
+++ b/Sources/Extensions/CGRect+Extensions.swift
@@ -1,32 +1,30 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-internal extension CGRect {
- init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) {
- self.init(x: x, y: y, width: w, height: h)
- }
+extension CGRect {
+ internal init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) {
+ self.init(x: x, y: y, width: w, height: h)
+ }
}
diff --git a/Sources/Extensions/MessageKind+textMessageKind.swift b/Sources/Extensions/MessageKind+textMessageKind.swift
index af3de70ac..bd9a31b21 100644
--- a/Sources/Extensions/MessageKind+textMessageKind.swift
+++ b/Sources/Extensions/MessageKind+textMessageKind.swift
@@ -1,38 +1,36 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-internal extension MessageKind {
- var textMessageKind: MessageKind {
- switch self {
- case .linkPreview(let linkItem):
- return linkItem.textKind
- case .text, .emoji, .attributedText:
- return self
- default:
- fatalError("textMessageKind not supported for messageKind: \(self)")
- }
+extension MessageKind {
+ internal var textMessageKind: MessageKind {
+ switch self {
+ case .linkPreview(let linkItem):
+ return linkItem.textKind
+ case .text, .emoji, .attributedText:
+ return self
+ default:
+ fatalError("textMessageKind not supported for messageKind: \(self)")
}
+ }
}
diff --git a/Sources/Extensions/MessageKit+Availability.swift b/Sources/Extensions/MessageKit+Availability.swift
new file mode 100644
index 000000000..07c051214
--- /dev/null
+++ b/Sources/Extensions/MessageKit+Availability.swift
@@ -0,0 +1,88 @@
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+extension MessagesLayoutDelegate {
+ public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize {
+ fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func avatarPosition(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> AvatarPosition {
+ fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func messageLabelInset(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets {
+ fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0")
+ }
+
+ public func messagePadding(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets {
+ fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func cellTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment {
+ fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func cellBottomLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment {
+ fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func widthForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
+ fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func heightForMedia(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func widthForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func heightForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ with _: CGFloat,
+ in _: MessagesCollectionView)
+ -> CGFloat
+ {
+ fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
+ }
+
+ public func shouldCacheLayoutAttributes(for _: MessageType) -> Bool {
+ fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.")
+ }
+}
diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift
index 7ac6e4bb5..87b15671f 100644
--- a/Sources/Extensions/NSAttributedString+Extensions.swift
+++ b/Sources/Extensions/NSAttributedString+Extensions.swift
@@ -1,37 +1,51 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-internal extension NSAttributedString {
-
- func width(considering height: CGFloat) -> CGFloat {
-
- let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
- let rect = self.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
- return rect.width
-
- }
+extension NSAttributedString {
+ public func width(considering height: CGFloat) -> CGFloat {
+ let size = size(consideringHeight: height)
+ return size.width
+ }
+
+ public func height(considering width: CGFloat) -> CGFloat {
+ let size = size(consideringWidth: width)
+ return size.height
+ }
+
+ public func size(consideringHeight height: CGFloat) -> CGSize {
+ let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
+ return size(considering: constraintBox)
+ }
+
+ public func size(consideringWidth width: CGFloat) -> CGSize {
+ let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude)
+ return size(considering: constraintBox)
+ }
+
+ public func size(considering size: CGSize) -> CGSize {
+ let rect = boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
+ return rect.size
+ }
}
diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift
index 01882f5e3..e5df7ad80 100644
--- a/Sources/Extensions/UIColor+Extensions.swift
+++ b/Sources/Extensions/UIColor+Extensions.swift
@@ -1,57 +1,58 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-internal extension UIColor {
+@MainActor
+extension UIColor {
+ // MARK: Internal
- private static func colorFromAssetBundle(named: String) -> UIColor {
- guard let color = UIColor(named: named, in: Bundle.messageKitAssetBundle, compatibleWith: nil) else {
- fatalError(MessageKitError.couldNotFindColorAsset)
- }
- return color
- }
-
- static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") }
-
- static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") }
-
- static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") }
-
- static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") }
-
- static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") }
-
- static var outgoingAudioMessageTint: UIColor { colorFromAssetBundle(named: "outgoingAudioMessageTint") }
-
- static var collectionViewBackground: UIColor { colorFromAssetBundle(named: "collectionViewBackground") }
-
- static var typingIndicatorDot: UIColor { colorFromAssetBundle(named: "typingIndicatorDot") }
-
- static var label: UIColor { colorFromAssetBundle(named: "label") }
-
- static var avatarViewBackground: UIColor { colorFromAssetBundle(named: "avatarViewBackground") }
+ internal static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") }
+
+ internal static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") }
+
+ internal static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") }
+
+ internal static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") }
+
+ internal static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") }
+
+ internal static var outgoingAudioMessageTint: UIColor { colorFromAssetBundle(named: "outgoingAudioMessageTint") }
+ internal static var collectionViewBackground: UIColor { colorFromAssetBundle(named: "collectionViewBackground") }
+
+ internal static var typingIndicatorDot: UIColor { colorFromAssetBundle(named: "typingIndicatorDot") }
+
+ internal static var label: UIColor { colorFromAssetBundle(named: "label") }
+
+ internal static var avatarViewBackground: UIColor { colorFromAssetBundle(named: "avatarViewBackground") }
+
+ // MARK: Private
+
+ private static func colorFromAssetBundle(named: String) -> UIColor {
+ guard let color = UIColor(named: named, in: Bundle.messageKitAssetBundle, compatibleWith: nil) else {
+ fatalError(MessageKitError.couldNotFindColorAsset)
+ }
+ return color
+ }
}
diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift
index ece1eb7c0..c26740de5 100644
--- a/Sources/Extensions/UIEdgeInsets+Extensions.swift
+++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift
@@ -1,38 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-internal extension UIEdgeInsets {
-
- var vertical: CGFloat {
- return top + bottom
- }
-
- var horizontal: CGFloat {
- return left + right
- }
+extension UIEdgeInsets {
+ internal var vertical: CGFloat {
+ top + bottom
+ }
+ internal var horizontal: CGFloat {
+ left + right
+ }
}
diff --git a/Sources/Extensions/UIImage+Extensions.swift b/Sources/Extensions/UIImage+Extensions.swift
index 35969b869..aa371f67e 100644
--- a/Sources/Extensions/UIImage+Extensions.swift
+++ b/Sources/Extensions/UIImage+Extensions.swift
@@ -1,38 +1,38 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
+// MARK: - ImageType
+
public enum ImageType: String {
- case play
- case pause
- case disclosure
+ case play
+ case pause
+ case disclosure
}
/// This extension provide a way to access image resources with in framework
-internal extension UIImage {
- static func messageKitImageWith(type: ImageType) -> UIImage? {
- UIImage(named: type.rawValue, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
- }
+extension UIImage {
+ internal static func messageKitImageWith(type: ImageType) -> UIImage? {
+ UIImage(named: type.rawValue, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
+ }
}
diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift
index 42bbbbd9c..d741de44b 100644
--- a/Sources/Extensions/UIView+Extensions.swift
+++ b/Sources/Extensions/UIView+Extensions.swift
@@ -1,128 +1,142 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
-internal extension UIView {
-
- func fillSuperview() {
- guard let superview = self.superview else {
- return
- }
- translatesAutoresizingMaskIntoConstraints = false
-
- let constraints: [NSLayoutConstraint] = [
- leftAnchor.constraint(equalTo: superview.leftAnchor),
- rightAnchor.constraint(equalTo: superview.rightAnchor),
- topAnchor.constraint(equalTo: superview.topAnchor),
- bottomAnchor.constraint(equalTo: superview.bottomAnchor)
- ]
- NSLayoutConstraint.activate(constraints)
+extension UIView {
+ internal func fillSuperview() {
+ guard let superview = superview else {
+ return
}
+ translatesAutoresizingMaskIntoConstraints = false
- func centerInSuperview() {
- guard let superview = self.superview else {
- return
- }
- translatesAutoresizingMaskIntoConstraints = false
- let constraints: [NSLayoutConstraint] = [
- centerXAnchor.constraint(equalTo: superview.centerXAnchor),
- centerYAnchor.constraint(equalTo: superview.centerYAnchor)
- ]
- NSLayoutConstraint.activate(constraints)
+ let constraints: [NSLayoutConstraint] = [
+ leftAnchor.constraint(equalTo: superview.leftAnchor),
+ rightAnchor.constraint(equalTo: superview.rightAnchor),
+ topAnchor.constraint(equalTo: superview.topAnchor),
+ bottomAnchor.constraint(equalTo: superview.bottomAnchor),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ internal func centerInSuperview() {
+ guard let superview = superview else {
+ return
+ }
+ translatesAutoresizingMaskIntoConstraints = false
+ let constraints: [NSLayoutConstraint] = [
+ centerXAnchor.constraint(equalTo: superview.centerXAnchor),
+ centerYAnchor.constraint(equalTo: superview.centerYAnchor),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ internal func constraint(equalTo size: CGSize) {
+ guard superview != nil else { return }
+ translatesAutoresizingMaskIntoConstraints = false
+ let constraints: [NSLayoutConstraint] = [
+ widthAnchor.constraint(equalToConstant: size.width),
+ heightAnchor.constraint(equalToConstant: size.height),
+ ]
+ NSLayoutConstraint.activate(constraints)
+ }
+
+ @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]
+ {
+ if superview == nil {
+ return []
+ }
+ translatesAutoresizingMaskIntoConstraints = false
+
+ var constraints = [NSLayoutConstraint]()
+
+ if let top = top {
+ let constraint = topAnchor.constraint(equalTo: top, constant: topConstant)
+ constraint.identifier = "top"
+ constraints.append(constraint)
+ }
+
+ if let left = left {
+ let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant)
+ constraint.identifier = "left"
+ constraints.append(constraint)
+ }
+
+ if let bottom = bottom {
+ let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant)
+ constraint.identifier = "bottom"
+ constraints.append(constraint)
+ }
+
+ if let right = right {
+ let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant)
+ constraint.identifier = "right"
+ constraints.append(constraint)
+ }
+
+ if let centerY = centerY {
+ let constraint = centerYAnchor.constraint(equalTo: centerY, constant: centerYConstant)
+ constraint.identifier = "centerY"
+ constraints.append(constraint)
}
-
- func constraint(equalTo size: CGSize) {
- guard superview != nil else { return }
- translatesAutoresizingMaskIntoConstraints = false
- let constraints: [NSLayoutConstraint] = [
- widthAnchor.constraint(equalToConstant: size.width),
- heightAnchor.constraint(equalToConstant: size.height)
- ]
- NSLayoutConstraint.activate(constraints)
-
+
+ if let centerX = centerX {
+ let constraint = centerXAnchor.constraint(equalTo: centerX, constant: centerXConstant)
+ constraint.identifier = "centerX"
+ constraints.append(constraint)
+ }
+
+ if widthConstant > 0 {
+ let constraint = widthAnchor.constraint(equalToConstant: widthConstant)
+ constraint.identifier = "width"
+ constraints.append(constraint)
}
- @discardableResult
- 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 []
- }
- translatesAutoresizingMaskIntoConstraints = false
-
- var constraints = [NSLayoutConstraint]()
-
- if let top = top {
- let constraint = topAnchor.constraint(equalTo: top, constant: topConstant)
- constraint.identifier = "top"
- constraints.append(constraint)
- }
-
- if let left = left {
- let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant)
- constraint.identifier = "left"
- constraints.append(constraint)
- }
-
- if let bottom = bottom {
- let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant)
- constraint.identifier = "bottom"
- constraints.append(constraint)
- }
-
- if let right = right {
- let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant)
- constraint.identifier = "right"
- constraints.append(constraint)
- }
-
- if let centerY = centerY {
- let constraint = centerYAnchor.constraint(equalTo: centerY, constant: centerYConstant)
- constraint.identifier = "centerY"
- constraints.append(constraint)
- }
-
- if let centerX = centerX {
- let constraint = centerXAnchor.constraint(equalTo: centerX, constant: centerXConstant)
- constraint.identifier = "centerX"
- constraints.append(constraint)
- }
-
- if widthConstant > 0 {
- let constraint = widthAnchor.constraint(equalToConstant: widthConstant)
- constraint.identifier = "width"
- constraints.append(constraint)
- }
-
- if heightConstant > 0 {
- let constraint = heightAnchor.constraint(equalToConstant: heightConstant)
- constraint.identifier = "height"
- constraints.append(constraint)
- }
-
- NSLayoutConstraint.activate(constraints)
- return constraints
+ if heightConstant > 0 {
+ let constraint = heightAnchor.constraint(equalToConstant: heightConstant)
+ constraint.identifier = "height"
+ constraints.append(constraint)
}
+
+ NSLayoutConstraint.activate(constraints)
+ return constraints
+ }
+
+ internal func addSubviews(_ subviews: UIView...) {
+ subviews.forEach { addSubview($0) }
+ }
}
diff --git a/Sources/Layout/AudioMessageSizeCalculator.swift b/Sources/Layout/AudioMessageSizeCalculator.swift
index 07a239243..b08ed2995 100644
--- a/Sources/Layout/AudioMessageSizeCalculator.swift
+++ b/Sources/Layout/AudioMessageSizeCalculator.swift
@@ -1,44 +1,41 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class AudioMessageSizeCalculator: MessageSizeCalculator {
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- switch message.kind {
- case .audio(let item):
- let maxWidth = messageContainerMaxWidth(for: message)
- if maxWidth < item.size.width {
- // Maintain the ratio if width is too great
- let height = maxWidth * item.size.height / item.size.width
- return CGSize(width: maxWidth, height: height)
- }
- return item.size
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ switch message.kind {
+ case .audio(let item):
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ }
}
diff --git a/Sources/Layout/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift
index 3bff16cb3..85ffbffed 100644
--- a/Sources/Layout/CellSizeCalculator.swift
+++ b/Sources/Layout/CellSizeCalculator.swift
@@ -1,50 +1,53 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// An object is responsible for
/// sizing and configuring cells for given `IndexPath`s.
+@MainActor
open class CellSizeCalculator {
+ // MARK: Lifecycle
- /// The layout object for which the cell size calculator is used.
- public weak var layout: UICollectionViewFlowLayout?
-
- /// Used to configure the layout attributes for a given cell.
- ///
- /// - Parameters:
- /// - attributes: The attributes of the cell.
- /// The default does nothing
- open func configure(attributes: UICollectionViewLayoutAttributes) {}
-
- /// Used to size an item at a given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the item to be displayed.
- /// The default return .zero
- open func sizeForItem(at indexPath: IndexPath) -> CGSize { return .zero }
-
- public init() {}
+ public init() { }
+ // MARK: Open
+
+ /// Used to configure the layout attributes for a given cell.
+ ///
+ /// - Parameters:
+ /// - attributes: The attributes of the cell.
+ /// The default does nothing
+ open func configure(attributes _: UICollectionViewLayoutAttributes) { }
+
+ /// Used to size an item at a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the item to be displayed.
+ /// The default return .zero
+ open func sizeForItem(at _: IndexPath) -> CGSize { .zero }
+
+ // MARK: Public
+
+ /// The layout object for which the cell size calculator is used.
+ public weak var layout: UICollectionViewFlowLayout?
}
diff --git a/Sources/Layout/ContactMessageSizeCalculator.swift b/Sources/Layout/ContactMessageSizeCalculator.swift
index 266a5cf4b..9d2166b29 100644
--- a/Sources/Layout/ContactMessageSizeCalculator.swift
+++ b/Sources/Layout/ContactMessageSizeCalculator.swift
@@ -1,74 +1,76 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class ContactMessageSizeCalculator: MessageSizeCalculator {
-
- public var incomingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 46, bottom: 7, right: 30)
- public var outgoingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 41, bottom: 7, right: 35)
- public var contactLabelFont = UIFont.preferredFont(forTextStyle: .body)
-
- internal func contactLabelInsets(for message: MessageType) -> UIEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageNameLabelInsets : incomingMessageNameLabelInsets
- }
-
- open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- let maxWidth = super.messageContainerMaxWidth(for: message)
- let textInsets = contactLabelInsets(for: message)
- return maxWidth - textInsets.horizontal
- }
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- let maxWidth = messageContainerMaxWidth(for: message)
-
- var messageContainerSize: CGSize
- let attributedText: NSAttributedString
-
- switch message.kind {
- case .contact(let item):
- attributedText = NSAttributedString(string: item.displayName, attributes: [.font: contactLabelFont])
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
-
- messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
-
- let messageInsets = contactLabelInsets(for: message)
- messageContainerSize.width += messageInsets.horizontal
- messageContainerSize.height += messageInsets.vertical
-
- return messageContainerSize
- }
-
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- super.configure(attributes: attributes)
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
- attributes.messageLabelFont = contactLabelFont
+ // MARK: Open
+
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ let textInsets = contactLabelInsets(for: message)
+ return maxWidth - textInsets.horizontal
+ }
+
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+
+ var messageContainerSize: CGSize
+ let attributedText: NSAttributedString
+
+ switch message.kind {
+ case .contact(let item):
+ attributedText = NSAttributedString(string: item.displayName, attributes: [.font: contactLabelFont])
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
+
+ let messageInsets = contactLabelInsets(for: message)
+ messageContainerSize.width += messageInsets.horizontal
+ messageContainerSize.height += messageInsets.vertical
+
+ return messageContainerSize
+ }
+
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+ attributes.messageLabelFont = contactLabelFont
+ }
+
+ // MARK: Public
+
+ public var incomingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 46, bottom: 7, right: 30)
+ public var outgoingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 41, bottom: 7, right: 35)
+ public var contactLabelFont = UIFont.preferredFont(forTextStyle: .body)
+
+ // MARK: Internal
+
+ internal func contactLabelInsets(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageNameLabelInsets : incomingMessageNameLabelInsets
+ }
}
diff --git a/Sources/Layout/LinkPreviewMessageSizeCalculator.swift b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift
index b9af0018f..b18f241a0 100644
--- a/Sources/Layout/LinkPreviewMessageSizeCalculator.swift
+++ b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift
@@ -1,102 +1,120 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
+// MARK: - LinkPreviewMessageSizeCalculator
+
open class LinkPreviewMessageSizeCalculator: TextMessageSizeCalculator {
+ // MARK: Lifecycle
- static let imageViewSize: CGFloat = 60
- static let imageViewMargin: CGFloat = 8
+ public override init(layout: MessagesCollectionViewFlowLayout?) {
+ let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold)
+ let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote)
+ self.titleFont = titleFontMetrics.scaledFont(for: titleFont)
- public var titleFont: UIFont
- public var teaserFont: UIFont = .preferredFont(forTextStyle: .caption2)
- public var domainFont: UIFont
+ let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold)
+ let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1)
+ self.domainFont = domainFontMetrics.scaledFont(for: domainFont)
- public override init(layout: MessagesCollectionViewFlowLayout?) {
- let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold)
- let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote)
- self.titleFont = titleFontMetrics.scaledFont(for: titleFont)
+ super.init(layout: layout)
+ }
- let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold)
- let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1)
- self.domainFont = domainFontMetrics.scaledFont(for: domainFont)
+ // MARK: Open
- super.init(layout: layout)
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ switch message.kind {
+ case .linkPreview:
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ return max(maxWidth, (layout?.collectionView?.bounds.width ?? 0) * 0.75)
+ default:
+ return super.messageContainerMaxWidth(for: message, at: indexPath)
}
+ }
- open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- switch message.kind {
- case .linkPreview:
- let maxWidth = super.messageContainerMaxWidth(for: message)
- return max(maxWidth, (layout?.collectionView?.bounds.width ?? 0) * 0.75)
- default:
- return super.messageContainerMaxWidth(for: message)
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ guard case MessageKind.linkPreview(let linkItem) = message.kind else {
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- guard case MessageKind.linkPreview(let linkItem) = message.kind else {
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ var containerSize = super.messageContainerSize(for: message, at: indexPath)
+ containerSize.width = max(containerSize.width, messageContainerMaxWidth(for: message, at: indexPath))
- var containerSize = super.messageContainerSize(for: message)
- containerSize.width = max(containerSize.width, messageContainerMaxWidth(for: message))
+ let labelInsets: UIEdgeInsets = messageLabelInsets(for: message)
- let labelInsets: UIEdgeInsets = messageLabelInsets(for: message)
+ let minHeight = containerSize.height + LinkPreviewMessageSizeCalculator.imageViewSize
+ let previewMaxWidth = containerSize
+ .width -
+ (
+ LinkPreviewMessageSizeCalculator.imageViewSize + LinkPreviewMessageSizeCalculator.imageViewMargin + labelInsets
+ .horizontal)
- let minHeight = containerSize.height + LinkPreviewMessageSizeCalculator.imageViewSize
- let previewMaxWidth = containerSize.width - (LinkPreviewMessageSizeCalculator.imageViewSize + LinkPreviewMessageSizeCalculator.imageViewMargin + labelInsets.horizontal)
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.title ?? "", attributes: [.font: titleFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
- calculateContainerSize(with: NSAttributedString(string: linkItem.title ?? "", attributes: [.font: titleFont]),
- containerSize: &containerSize,
- maxWidth: previewMaxWidth)
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.teaser, attributes: [.font: teaserFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
- calculateContainerSize(with: NSAttributedString(string: linkItem.teaser, attributes: [.font: teaserFont]),
- containerSize: &containerSize,
- maxWidth: previewMaxWidth)
+ calculateContainerSize(
+ with: NSAttributedString(string: linkItem.url.host ?? "", attributes: [.font: domainFont]),
+ containerSize: &containerSize,
+ maxWidth: previewMaxWidth)
- calculateContainerSize(with: NSAttributedString(string: linkItem.url.host ?? "", attributes: [.font: domainFont]),
- containerSize: &containerSize,
- maxWidth: previewMaxWidth)
+ containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical
- containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical
+ return containerSize
+ }
- return containerSize
- }
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+ attributes.linkPreviewFonts = LinkPreviewFonts(titleFont: titleFont, teaserFont: teaserFont, domainFont: domainFont)
+ }
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- super.configure(attributes: attributes)
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
- attributes.linkPreviewFonts = LinkPreviewFonts(titleFont: titleFont, teaserFont: teaserFont, domainFont: domainFont)
- }
+ // MARK: Public
+
+ public var titleFont: UIFont
+ public var teaserFont: UIFont = .preferredFont(forTextStyle: .caption2)
+ public var domainFont: UIFont
+
+ // MARK: Internal
+
+ static let imageViewSize: CGFloat = 60
+ static let imageViewMargin: CGFloat = 8
}
-private extension LinkPreviewMessageSizeCalculator {
- private func calculateContainerSize(with attibutedString: NSAttributedString, containerSize: inout CGSize, maxWidth: CGFloat) {
- guard !attibutedString.string.isEmpty else { return }
- let size = labelSize(for: attibutedString, considering: maxWidth)
- containerSize.height += size.height
- }
+extension LinkPreviewMessageSizeCalculator {
+ private func calculateContainerSize(
+ with attributedString: NSAttributedString,
+ containerSize: inout CGSize,
+ maxWidth: CGFloat)
+ {
+ guard !attributedString.string.isEmpty else { return }
+ let size = labelSize(for: attributedString, considering: maxWidth)
+ containerSize.height += size.height
+ }
}
diff --git a/Sources/Layout/LocationMessageSizeCalculator.swift b/Sources/Layout/LocationMessageSizeCalculator.swift
index e862b9c67..56ce3329c 100644
--- a/Sources/Layout/LocationMessageSizeCalculator.swift
+++ b/Sources/Layout/LocationMessageSizeCalculator.swift
@@ -1,44 +1,41 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class LocationMessageSizeCalculator: MessageSizeCalculator {
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- switch message.kind {
- case .location(let item):
- let maxWidth = messageContainerMaxWidth(for: message)
- if maxWidth < item.size.width {
- // Maintain the ratio if width is too great
- let height = maxWidth * item.size.height / item.size.width
- return CGSize(width: maxWidth, height: height)
- }
- return item.size
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ switch message.kind {
+ case .location(let item):
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ }
}
diff --git a/Sources/Layout/MediaMessageSizeCalculator.swift b/Sources/Layout/MediaMessageSizeCalculator.swift
index 46ff9f1e5..c6eebaf91 100644
--- a/Sources/Layout/MediaMessageSizeCalculator.swift
+++ b/Sources/Layout/MediaMessageSizeCalculator.swift
@@ -1,49 +1,46 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class MediaMessageSizeCalculator: MessageSizeCalculator {
-
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- let maxWidth = messageContainerMaxWidth(for: message)
- let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in
- if maxWidth < item.size.width {
- // Maintain the ratio if width is too great
- let height = maxWidth * item.size.height / item.size.width
- return CGSize(width: maxWidth, height: height)
- }
- return item.size
- }
- switch message.kind {
- case .photo(let item):
- return sizeForMediaItem(maxWidth, item)
- case .video(let item):
- return sizeForMediaItem(maxWidth, item)
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+ let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in
+ if maxWidth < item.size.width {
+ // Maintain the ratio if width is too great
+ let height = maxWidth * item.size.height / item.size.width
+ return CGSize(width: maxWidth, height: height)
+ }
+ return item.size
+ }
+ switch message.kind {
+ case .photo(let item):
+ return sizeForMediaItem(maxWidth, item)
+ case .video(let item):
+ return sizeForMediaItem(maxWidth, item)
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
}
+ }
}
diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift
index dad694114..099487215 100644
--- a/Sources/Layout/MessageSizeCalculator.swift
+++ b/Sources/Layout/MessageSizeCalculator.swift
@@ -1,305 +1,353 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-open class MessageSizeCalculator: CellSizeCalculator {
+// MARK: - MessageSizeCalculator
- public init(layout: MessagesCollectionViewFlowLayout? = nil) {
- super.init()
-
- self.layout = layout
+open class MessageSizeCalculator: CellSizeCalculator {
+ // MARK: Lifecycle
+
+ public init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+
+ self.layout = layout
+ }
+
+ // MARK: Open
+
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+
+ let dataSource = messagesLayout.messagesDataSource
+ let indexPath = attributes.indexPath
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
+
+ attributes.avatarSize = avatarSize(for: message, at: indexPath)
+ attributes.avatarPosition = avatarPosition(for: message)
+ attributes.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
+
+ attributes.messageContainerPadding = messageContainerPadding(for: message)
+ attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath)
+ attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath)
+ attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: message)
+ attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath)
+ attributes.messageTimeLabelSize = messageTimeLabelSize(for: message, at: indexPath)
+ attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message)
+ attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath)
+ attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message, at: indexPath)
+
+ attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message, at: indexPath)
+ attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath)
+
+ attributes.accessoryViewSize = accessoryViewSize(for: message)
+ attributes.accessoryViewPadding = accessoryViewPadding(for: message)
+ attributes.accessoryViewPosition = accessoryViewPosition(for: message)
+ }
+
+ open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
+ let itemHeight = cellContentHeight(for: message, at: indexPath)
+ return CGSize(width: messagesLayout.itemWidth, height: itemHeight)
+ }
+
+ open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height
+ let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height
+ let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height
+ let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height
+ let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height
+ let messageVerticalPadding = messageContainerPadding(for: message).vertical
+ let avatarHeight = avatarSize(for: message, at: indexPath).height
+ let avatarVerticalPosition = avatarPosition(for: message).vertical
+ let accessoryViewHeight = accessoryViewSize(for: message).height
+
+ switch avatarVerticalPosition {
+ case .messageCenter:
+ let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
+ + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ let cellHeight = max(avatarHeight, totalLabelHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageBottom:
+ var cellHeight: CGFloat = 0
+ cellHeight += messageBottomLabelHeight
+ cellHeight += cellBottomLabelHeight
+ let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight
+ cellHeight += max(labelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageTop:
+ var cellHeight: CGFloat = 0
+ cellHeight += cellTopLabelHeight
+ cellHeight += messageTopLabelHeight
+ let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ cellHeight += max(labelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .messageLabelTop:
+ var cellHeight: CGFloat = 0
+ cellHeight += cellTopLabelHeight
+ let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding +
+ messageTopLabelHeight + cellBottomLabelHeight
+ cellHeight += max(messageLabelsHeight, avatarHeight)
+ return max(cellHeight, accessoryViewHeight)
+ case .cellTop, .cellBottom:
+ let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
+ + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
+ let cellHeight = max(avatarHeight, totalLabelHeight)
+ return max(cellHeight, accessoryViewHeight)
}
+ }
- public var incomingAvatarSize = CGSize(width: 30, height: 30)
- public var outgoingAvatarSize = CGSize(width: 30, height: 30)
-
- public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom)
- public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom)
-
- public var avatarLeadingTrailingPadding: CGFloat = 0
-
- public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)
- public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
-
- public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
-
- public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
- public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
-
- public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
- public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
-
- public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
- public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
-
- public var incomingAccessoryViewSize = CGSize.zero
- public var outgoingAccessoryViewSize = CGSize.zero
+ // MARK: - Avatar
- public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero
- public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero
-
- public var incomingAccessoryViewPosition: AccessoryPosition = .messageCenter
- public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter
+ open func avatarPosition(for message: MessageType) -> AvatarPosition {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
-
- let dataSource = messagesLayout.messagesDataSource
- let indexPath = attributes.indexPath
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
-
- attributes.avatarSize = avatarSize(for: message)
- attributes.avatarPosition = avatarPosition(for: message)
- attributes.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
-
- 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.messageTimeLabelSize = messageTimeLabelSize(for: message, at: indexPath)
- attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message)
- attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath)
- attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message)
-
- attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message)
- attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath)
-
- attributes.accessoryViewSize = accessoryViewSize(for: message)
- attributes.accessoryViewPadding = accessoryViewPadding(for: message)
- attributes.accessoryViewPosition = accessoryViewPosition(for: message)
+ switch position.horizontal {
+ case .cellTrailing, .cellLeading:
+ break
+ case .natural:
+ position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading
}
-
- open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
- let itemHeight = cellContentHeight(for: message, at: indexPath)
- return CGSize(width: messagesLayout.itemWidth, height: itemHeight)
+ return position
+ }
+
+ open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ if let size = layoutDelegate.avatarSize(for: message, at: indexPath, in: collectionView) {
+ return size
}
-
- open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
-
- let messageContainerHeight = messageContainerSize(for: message).height
- let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height
- let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height
- let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height
- let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height
- let messageVerticalPadding = messageContainerPadding(for: message).vertical
- let avatarHeight = avatarSize(for: message).height
- let avatarVerticalPosition = avatarPosition(for: message).vertical
- let accessoryViewHeight = accessoryViewSize(for: message).height
-
- switch avatarVerticalPosition {
- case .messageCenter:
- let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
- + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
- let cellHeight = max(avatarHeight, totalLabelHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageBottom:
- var cellHeight: CGFloat = 0
- cellHeight += messageBottomLabelHeight
- cellHeight += cellBottomLabelHeight
- let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight
- cellHeight += max(labelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageTop:
- var cellHeight: CGFloat = 0
- cellHeight += cellTopLabelHeight
- cellHeight += messageTopLabelHeight
- let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
- cellHeight += max(labelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .messageLabelTop:
- var cellHeight: CGFloat = 0
- cellHeight += cellTopLabelHeight
- let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + messageTopLabelHeight + cellBottomLabelHeight
- cellHeight += max(messageLabelsHeight, avatarHeight)
- return max(cellHeight, accessoryViewHeight)
- case .cellTop, .cellBottom:
- let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight
- + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight
- let cellHeight = max(avatarHeight, totalLabelHeight)
- return max(cellHeight, accessoryViewHeight)
- }
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize
+ }
+
+ // MARK: - Top cell Label
+
+ open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment
+ }
+
+ // MARK: - Top message Label
+
+ open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func messageTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment {
+ let collectionView = messagesLayout.messagesCollectionView
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+
+ if let alignment = layoutDelegate.messageTopLabelAlignment(for: message, at: indexPath, in: collectionView) {
+ return alignment
}
- // MARK: - Avatar
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment
+ }
- open func avatarPosition(for message: MessageType) -> AvatarPosition {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition
+ // MARK: - Message time label
- switch position.horizontal {
- case .cellTrailing, .cellLeading:
- break
- case .natural:
- position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading
- }
- return position
+ open func messageTimeLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ guard let attributedText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) else {
+ return .zero
}
-
- open func avatarSize(for message: MessageType) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize
+ let size = attributedText.size()
+ return CGSize(width: size.width, height: size.height)
+ }
+
+ // MARK: - Bottom cell Label
+
+ open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment
+ }
+
+ // MARK: - Bottom Message Label
+
+ open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+ let collectionView = messagesLayout.messagesCollectionView
+ let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView)
+ return CGSize(width: messagesLayout.itemWidth, height: height)
+ }
+
+ open func messageBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment {
+ let collectionView = messagesLayout.messagesCollectionView
+ let layoutDelegate = messagesLayout.messagesLayoutDelegate
+
+ if let alignment = layoutDelegate.messageBottomLabelAlignment(for: message, at: indexPath, in: collectionView) {
+ return alignment
}
- // MARK: - Top cell Label
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment
+ }
- open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
+ // MARK: - MessageContainer
- open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment
- }
-
- // MARK: - Top message Label
-
- open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
-
- open func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment
- }
+ open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding
+ }
- // MARK: - Message time label
+ open func messageContainerSize(for _: MessageType, at _: IndexPath) -> CGSize {
+ // Returns .zero by default
+ .zero
+ }
- open func messageTimeLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- guard let attributedText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) else {
- return .zero
- }
- let size = attributedText.size()
- return CGSize(width: size.width, height: size.height)
- }
+ open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let avatarWidth: CGFloat = avatarSize(for: message, at: indexPath).width
+ let messagePadding = messageContainerPadding(for: message)
+ let accessoryWidth = accessoryViewSize(for: message).width
+ let accessoryPadding = accessoryViewPadding(for: message)
+ return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding
+ .horizontal - avatarLeadingTrailingPadding
+ }
- // MARK: - Bottom cell Label
-
- open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
-
- open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment
- }
+ // MARK: Public
- // MARK: - Bottom Message Label
+ public var incomingAvatarSize = CGSize(width: 30, height: 30)
+ public var outgoingAvatarSize = CGSize(width: 30, height: 30)
- open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
- let layoutDelegate = messagesLayout.messagesLayoutDelegate
- let collectionView = messagesLayout.messagesCollectionView
- let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView)
- return CGSize(width: messagesLayout.itemWidth, height: height)
- }
+ public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom)
+ public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom)
- open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment
- }
+ public var avatarLeadingTrailingPadding: CGFloat = 0
- // MARK: - Accessory View
+ public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)
+ public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
- public func accessoryViewSize(for message: MessageType) -> CGSize {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize
- }
+ public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding
- }
-
- public func accessoryViewPosition(for message: MessageType) -> AccessoryPosition {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingAccessoryViewPosition : incomingAccessoryViewPosition
- }
+ public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- // MARK: - MessageContainer
-
- open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding
- }
+ public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- open func messageContainerSize(for message: MessageType) -> CGSize {
- // Returns .zero by default
- return .zero
- }
+ public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42))
+ public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42))
- open func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- let avatarWidth = avatarSize(for: message).width
- let messagePadding = messageContainerPadding(for: message)
- let accessoryWidth = accessoryViewSize(for: message).width
- let accessoryPadding = accessoryViewPadding(for: message)
- return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding.horizontal - avatarLeadingTrailingPadding
- }
+ public var incomingAccessoryViewSize = CGSize.zero
+ public var outgoingAccessoryViewSize = CGSize.zero
- // MARK: - Helpers
+ public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero
+ public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero
- public var messagesLayout: MessagesCollectionViewFlowLayout {
- guard let layout = layout as? MessagesCollectionViewFlowLayout else {
- fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout")
- }
- return layout
- }
+ public var incomingAccessoryViewPosition: AccessoryPosition = .messageCenter
+ public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter
- 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
+ // MARK: - Helpers
- return rect.size
+ public var messagesLayout: MessagesCollectionViewFlowLayout {
+ guard let layout = layout as? MessagesCollectionViewFlowLayout else {
+ fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout")
}
+ return layout
+ }
+
+ // MARK: - Accessory View
+
+ public func accessoryViewSize(for message: MessageType) -> CGSize {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize
+ }
+
+ public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding
+ }
+
+ public func accessoryViewPosition(for message: MessageType) -> AccessoryPosition {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingAccessoryViewPosition : incomingAccessoryViewPosition
+ }
+
+ // 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)
+
+ 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))
+ }
}
-fileprivate extension UIEdgeInsets {
- init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
- self.init(top: top, left: left, bottom: bottom, right: right)
- }
+extension UIEdgeInsets {
+ fileprivate init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
+ self.init(top: top, left: left, bottom: bottom, right: right)
+ }
}
diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift
index 8970e8ce1..fa280b932 100644
--- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift
+++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift
@@ -1,333 +1,357 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+import AVFoundation
import Foundation
import UIKit
-import AVFoundation
/// The layout object used by `MessagesCollectionView` to determine the size of all
/// framework provided `MessageCollectionViewCell` subclasses.
open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
-
- open override class var layoutAttributesClass: AnyClass {
- return MessagesCollectionViewLayoutAttributes.self
- }
-
- /// The `MessagesCollectionView` that owns this layout object.
- public var messagesCollectionView: MessagesCollectionView {
- guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
- fatalError(MessageKitError.layoutUsedOnForeignType)
- }
- return messagesCollectionView
- }
-
- /// The `MessagesDataSource` for the layout's collection view.
- public var messagesDataSource: MessagesDataSource {
- guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- return messagesDataSource
- }
-
- /// The `MessagesLayoutDelegate` for the layout's collection view.
- public var messagesLayoutDelegate: MessagesLayoutDelegate {
- guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
- fatalError(MessageKitError.nilMessagesLayoutDelegate)
- }
- return messagesLayoutDelegate
- }
-
- public var itemWidth: CGFloat {
- guard let collectionView = collectionView else { return 0 }
- return collectionView.frame.width - sectionInset.left - sectionInset.right
- }
-
- public private(set) var isTypingIndicatorViewHidden: Bool = true
-
- // MARK: - Initializers
-
- public override init() {
- super.init()
- setupView()
- setupObserver()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupView()
- setupObserver()
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
-
- // MARK: - Methods
-
- private func setupView() {
- sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
- }
-
- private func setupObserver() {
- NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)
- }
-
- // MARK: - Typing Indicator API
-
- /// Notifies the layout that the typing indicator will change state
- ///
- /// - Parameters:
- /// - isHidden: A Boolean value that is to be the new state of the typing indicator
- internal func setTypingIndicatorViewHidden(_ isHidden: Bool) {
- isTypingIndicatorViewHidden = isHidden
- }
-
- /// A method that by default checks if the section is the last in the
- /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
- /// is FALSE
- ///
- /// - Parameter section
- /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
- open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
- return !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1
- }
-
- // MARK: - Attributes
-
- open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
- guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else {
- return nil
- }
- for attributes in attributesArray where attributes.representedElementCategory == .cell {
- let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
- cellSizeCalculator.configure(attributes: attributes)
- }
- return attributesArray
- }
-
- open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
- guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else {
- return nil
- }
- if attributes.representedElementCategory == .cell {
- let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
- cellSizeCalculator.configure(attributes: attributes)
- }
- return attributes
- }
-
- // MARK: - Layout Invalidation
-
- open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
- return collectionView?.bounds.width != newBounds.width
- }
-
- open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
- let context = super.invalidationContext(forBoundsChange: newBounds)
- guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
- flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds)
- return flowLayoutContext
- }
-
- @objc
- private func handleOrientationChange(_ notification: Notification) {
- invalidateLayout()
- }
-
- // MARK: - Cell Sizing
-
- lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
- lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
- lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = {
- let sizeCalculator = TextMessageSizeCalculator(layout: self)
- sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2)
- return sizeCalculator
- }()
- lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
- lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
- lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self)
- lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self)
- lazy open var contactMessageSizeCalculator = ContactMessageSizeCalculator(layout: self)
- lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self)
- lazy open var linkPreviewMessageSizeCalculator = LinkPreviewMessageSizeCalculator(layout: self)
-
- /// Note:
- /// - If you override this method, remember to call MessageLayoutDelegate's
- /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary
- /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator`
- /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator`
- open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
- if isSectionReservedForTypingIndicator(indexPath.section) {
- return typingIndicatorSizeCalculator
- }
- let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
- switch message.kind {
- case .text:
- return textMessageSizeCalculator
- case .attributedText:
- return attributedTextMessageSizeCalculator
- case .emoji:
- return emojiMessageSizeCalculator
- case .photo:
- return photoMessageSizeCalculator
- case .video:
- return videoMessageSizeCalculator
- case .location:
- return locationMessageSizeCalculator
- case .audio:
- return audioMessageSizeCalculator
- case .contact:
- return contactMessageSizeCalculator
- case .linkPreview:
- return linkPreviewMessageSizeCalculator
- case .custom:
- return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView)
- }
- }
-
- open func sizeForItem(at indexPath: IndexPath) -> CGSize {
- let calculator = cellSizeCalculatorForItem(at: indexPath)
- return calculator.sizeForItem(at: indexPath)
- }
-
- /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageIncomingAvatarSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize }
- }
-
- /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAvatarSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize }
- }
-
- /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s
- public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) {
- messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition }
- }
-
- /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) {
- messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition }
- }
-
- /// Set `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s
- public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) {
- messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding }
- }
-
- /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) {
- messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding }
- }
-
- /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) {
- messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding }
- }
-
- /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment }
- }
-
- /// Set `incomingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingCellBottomLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingCellBottomLabelAlignment = newAlignment }
- }
-
- /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment }
- }
-
- /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment }
- }
-
- /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
- public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
- messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment }
- }
-
- /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s
- public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize }
- }
-
- /// Set `outgoingAccessoryViewSize` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) {
- messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize }
- }
-
- /// Set `incomingAccessoryViewPadding` of all `MessageSizeCalculator`s
- public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
- messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding }
- }
-
- /// Set `outgoingAccessoryViewPadding` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
- messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding }
- }
-
- /// Set `incomingAccessoryViewPosition` of all `MessageSizeCalculator`s
- public func setMessageIncomingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
- messageSizeCalculators().forEach { $0.incomingAccessoryViewPosition = newPosition }
- }
-
- /// Set `outgoingAccessoryViewPosition` of all `MessageSizeCalculator`s
- public func setMessageOutgoingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
- messageSizeCalculators().forEach { $0.outgoingAccessoryViewPosition = newPosition }
- }
-
- /// Get all `MessageSizeCalculator`s
- open func messageSizeCalculators() -> [MessageSizeCalculator] {
- return [textMessageSizeCalculator,
- attributedTextMessageSizeCalculator,
- emojiMessageSizeCalculator,
- photoMessageSizeCalculator,
- videoMessageSizeCalculator,
- locationMessageSizeCalculator,
- audioMessageSizeCalculator,
- contactMessageSizeCalculator,
- linkPreviewMessageSizeCalculator
- ]
- }
-
+ // MARK: Lifecycle
+
+ // MARK: - Initializers
+
+ public override init() {
+ super.init()
+ setupView()
+ setupObserver()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupView()
+ setupObserver()
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ // MARK: Open
+
+ open override class var layoutAttributesClass: AnyClass {
+ MessagesCollectionViewLayoutAttributes.self
+ }
+
+ // MARK: - Cell Sizing
+
+ lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
+ lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self)
+ lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = {
+ let sizeCalculator = TextMessageSizeCalculator(layout: self)
+ sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2)
+ return sizeCalculator
+ }()
+
+ lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
+ lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
+ lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self)
+ lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self)
+ lazy open var contactMessageSizeCalculator = ContactMessageSizeCalculator(layout: self)
+ lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self)
+ lazy open var linkPreviewMessageSizeCalculator = LinkPreviewMessageSizeCalculator(layout: self)
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1
+ }
+
+ // MARK: - Attributes
+
+ open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
+ guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes]
+ else {
+ return nil
+ }
+ for attributes in attributesArray where attributes.representedElementCategory == .cell {
+ let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
+ cellSizeCalculator.configure(attributes: attributes)
+ }
+ return attributesArray
+ }
+
+ open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
+ guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else {
+ return nil
+ }
+ if attributes.representedElementCategory == .cell {
+ let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath)
+ cellSizeCalculator.configure(attributes: attributes)
+ }
+ return attributes
+ }
+
+ // MARK: - Layout Invalidation
+
+ open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
+ collectionView?.bounds.width != newBounds.width
+ }
+
+ open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
+ let context = super.invalidationContext(forBoundsChange: newBounds)
+ guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
+ flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds)
+ return flowLayoutContext
+ }
+
+ /// Note:
+ /// - If you override this method, remember to call MessageLayoutDelegate's
+ /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary
+ /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator`
+ /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator`
+ open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
+ if isSectionReservedForTypingIndicator(indexPath.section) {
+ return typingIndicatorSizeCalculator
+ }
+ let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
+ switch message.kind {
+ case .text:
+ return messagesLayoutDelegate
+ .textCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? textMessageSizeCalculator
+ case .attributedText:
+ return messagesLayoutDelegate.attributedTextCellSizeCalculator(
+ for: message,
+ at: indexPath,
+ in: messagesCollectionView) ?? attributedTextMessageSizeCalculator
+ case .emoji:
+ return messagesLayoutDelegate
+ .emojiCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? emojiMessageSizeCalculator
+ case .photo:
+ return messagesLayoutDelegate
+ .photoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? photoMessageSizeCalculator
+ case .video:
+ return messagesLayoutDelegate
+ .videoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? videoMessageSizeCalculator
+ case .location:
+ return messagesLayoutDelegate
+ .locationCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ??
+ locationMessageSizeCalculator
+ case .audio:
+ return messagesLayoutDelegate
+ .audioCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? audioMessageSizeCalculator
+ case .contact:
+ return messagesLayoutDelegate
+ .contactCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? contactMessageSizeCalculator
+ case .linkPreview:
+ return linkPreviewMessageSizeCalculator
+ case .custom:
+ return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView)
+ }
+ }
+
+ open func sizeForItem(at indexPath: IndexPath) -> CGSize {
+ let calculator = cellSizeCalculatorForItem(at: indexPath)
+ return calculator.sizeForItem(at: indexPath)
+ }
+
+ /// Get all `MessageSizeCalculator`s
+ open func messageSizeCalculators() -> [MessageSizeCalculator] {
+ [
+ textMessageSizeCalculator,
+ attributedTextMessageSizeCalculator,
+ emojiMessageSizeCalculator,
+ photoMessageSizeCalculator,
+ videoMessageSizeCalculator,
+ locationMessageSizeCalculator,
+ audioMessageSizeCalculator,
+ contactMessageSizeCalculator,
+ linkPreviewMessageSizeCalculator,
+ ]
+ }
+
+ // MARK: Public
+
+ public private(set) var isTypingIndicatorViewHidden = true
+
+ /// The `MessagesCollectionView` that owns this layout object.
+ public var messagesCollectionView: MessagesCollectionView {
+ guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
+ fatalError(MessageKitError.layoutUsedOnForeignType)
+ }
+ return messagesCollectionView
+ }
+
+ /// The `MessagesDataSource` for the layout's collection view.
+ public var messagesDataSource: MessagesDataSource {
+ guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
+ }
+ return messagesDataSource
+ }
+
+ /// The `MessagesLayoutDelegate` for the layout's collection view.
+ public var messagesLayoutDelegate: MessagesLayoutDelegate {
+ guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
+ fatalError(MessageKitError.nilMessagesLayoutDelegate)
+ }
+ return messagesLayoutDelegate
+ }
+
+ public var itemWidth: CGFloat {
+ guard let collectionView = collectionView else { return 0 }
+ return collectionView.frame.width - sectionInset.left - sectionInset.right
+ }
+
+ /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAvatarSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize }
+ }
+
+ /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAvatarSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize }
+ }
+
+ /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) {
+ messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition }
+ }
+
+ /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) {
+ messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition }
+ }
+
+ /// Set `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s
+ public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) {
+ messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding }
+ }
+
+ /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) {
+ messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding }
+ }
+
+ /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) {
+ messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding }
+ }
+
+ /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingCellBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingCellBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingCellBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) {
+ messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment }
+ }
+
+ /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize }
+ }
+
+ /// Set `outgoingAccessoryViewSize` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize }
+ }
+
+ /// Set `incomingAccessoryViewPadding` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding }
+ }
+
+ /// Set `outgoingAccessoryViewPadding` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding }
+ }
+
+ /// Set `incomingAccessoryViewPosition` of all `MessageSizeCalculator`s
+ public func setMessageIncomingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
+ messageSizeCalculators().forEach { $0.incomingAccessoryViewPosition = newPosition }
+ }
+
+ /// Set `outgoingAccessoryViewPosition` of all `MessageSizeCalculator`s
+ public func setMessageOutgoingAccessoryViewPosition(_ newPosition: AccessoryPosition) {
+ messageSizeCalculators().forEach { $0.outgoingAccessoryViewPosition = newPosition }
+ }
+
+ // MARK: Internal
+
+ // MARK: - Typing Indicator API
+
+ /// Notifies the layout that the typing indicator will change state
+ ///
+ /// - Parameters:
+ /// - isHidden: A Boolean value that is to be the new state of the typing indicator
+ internal func setTypingIndicatorViewHidden(_ isHidden: Bool) {
+ isTypingIndicatorViewHidden = isHidden
+ }
+
+ // MARK: Private
+
+ // MARK: - Methods
+
+ private func setupView() {
+ sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
+ }
+
+ private func setupObserver() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)),
+ name: UIDevice.orientationDidChangeNotification,
+ object: nil)
+ }
+
+ @objc
+ private func handleOrientationChange(_: Notification) {
+ invalidateLayout()
+ }
}
diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
index e182a63c9..ff8fefad6 100644
--- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
+++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift
@@ -1,119 +1,91 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// The layout attributes used by a `MessageCollectionViewCell` to layout its subviews.
-open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
+open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
+ // MARK: Open
- // MARK: - Properties
+ // MARK: - Methods
- public var avatarSize: CGSize = .zero
- public var avatarPosition = AvatarPosition(vertical: .cellBottom)
- public var avatarLeadingTrailingPadding: CGFloat = 0
+ open override func copy(with zone: NSZone? = nil) -> Any {
+ // swiftlint:disable force_cast
+ let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes
+ copy.avatarSize = avatarSize
+ copy.avatarPosition = avatarPosition
+ copy.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
+ copy.messageContainerSize = messageContainerSize
+ copy.messageContainerPadding = messageContainerPadding
+ copy.messageLabelFont = messageLabelFont
+ copy.messageLabelInsets = messageLabelInsets
+ copy.cellTopLabelAlignment = cellTopLabelAlignment
+ copy.cellTopLabelSize = cellTopLabelSize
+ copy.cellBottomLabelAlignment = cellBottomLabelAlignment
+ copy.cellBottomLabelSize = cellBottomLabelSize
+ copy.messageTimeLabelSize = messageTimeLabelSize
+ copy.messageTopLabelAlignment = messageTopLabelAlignment
+ copy.messageTopLabelSize = messageTopLabelSize
+ copy.messageBottomLabelAlignment = messageBottomLabelAlignment
+ copy.messageBottomLabelSize = messageBottomLabelSize
+ copy.accessoryViewSize = accessoryViewSize
+ copy.accessoryViewPadding = accessoryViewPadding
+ copy.accessoryViewPosition = accessoryViewPosition
+ copy.linkPreviewFonts = linkPreviewFonts
+ return copy
+ // swiftlint:enable force_cast
+ }
- public var messageContainerSize: CGSize = .zero
- public var messageContainerPadding: UIEdgeInsets = .zero
- public var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
- public var messageLabelInsets: UIEdgeInsets = .zero
+ // MARK: - Properties
- public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var cellTopLabelSize: CGSize = .zero
-
- public var cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var cellBottomLabelSize: CGSize = .zero
-
- public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var messageTopLabelSize: CGSize = .zero
+ public var avatarSize: CGSize = .zero
+ public var avatarPosition = AvatarPosition(vertical: .cellBottom)
+ public var avatarLeadingTrailingPadding: CGFloat = 0
- public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
- public var messageBottomLabelSize: CGSize = .zero
+ public var messageContainerSize: CGSize = .zero
+ public var messageContainerPadding: UIEdgeInsets = .zero
+ public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+ public var messageLabelInsets: UIEdgeInsets = .zero
- public var messageTimeLabelSize: CGSize = .zero
+ public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var cellTopLabelSize: CGSize = .zero
- public var accessoryViewSize: CGSize = .zero
- public var accessoryViewPadding: HorizontalEdgeInsets = .zero
- public var accessoryViewPosition: AccessoryPosition = .messageCenter
+ public var cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var cellBottomLabelSize: CGSize = .zero
- public var linkPreviewFonts = LinkPreviewFonts(titleFont: .preferredFont(forTextStyle: .footnote),
- teaserFont: .preferredFont(forTextStyle: .caption2),
- domainFont: .preferredFont(forTextStyle: .caption1))
-
- // MARK: - Methods
+ public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var messageTopLabelSize: CGSize = .zero
- open override func copy(with zone: NSZone? = nil) -> Any {
- // swiftlint:disable force_cast
- let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes
- copy.avatarSize = avatarSize
- copy.avatarPosition = avatarPosition
- copy.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding
- copy.messageContainerSize = messageContainerSize
- copy.messageContainerPadding = messageContainerPadding
- copy.messageLabelFont = messageLabelFont
- copy.messageLabelInsets = messageLabelInsets
- copy.cellTopLabelAlignment = cellTopLabelAlignment
- copy.cellTopLabelSize = cellTopLabelSize
- copy.cellBottomLabelAlignment = cellBottomLabelAlignment
- copy.cellBottomLabelSize = cellBottomLabelSize
- copy.messageTimeLabelSize = messageTimeLabelSize
- copy.messageTopLabelAlignment = messageTopLabelAlignment
- copy.messageTopLabelSize = messageTopLabelSize
- copy.messageBottomLabelAlignment = messageBottomLabelAlignment
- copy.messageBottomLabelSize = messageBottomLabelSize
- copy.accessoryViewSize = accessoryViewSize
- copy.accessoryViewPadding = accessoryViewPadding
- copy.accessoryViewPosition = accessoryViewPosition
- copy.linkPreviewFonts = linkPreviewFonts
- return copy
- // swiftlint:enable force_cast
- }
+ public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero)
+ public var messageBottomLabelSize: CGSize = .zero
- open override func isEqual(_ object: Any?) -> Bool {
- // MARK: - LEAVE this as is
- if let attributes = object as? MessagesCollectionViewLayoutAttributes {
- return super.isEqual(object) && attributes.avatarSize == avatarSize
- && attributes.avatarPosition == avatarPosition
- && attributes.avatarLeadingTrailingPadding == avatarLeadingTrailingPadding
- && attributes.messageContainerSize == messageContainerSize
- && attributes.messageContainerPadding == messageContainerPadding
- && attributes.messageLabelFont == messageLabelFont
- && attributes.messageLabelInsets == messageLabelInsets
- && attributes.cellTopLabelAlignment == cellTopLabelAlignment
- && attributes.cellTopLabelSize == cellTopLabelSize
- && attributes.cellBottomLabelAlignment == cellBottomLabelAlignment
- && attributes.cellBottomLabelSize == cellBottomLabelSize
- && attributes.messageTimeLabelSize == messageTimeLabelSize
- && attributes.messageTopLabelAlignment == messageTopLabelAlignment
- && attributes.messageTopLabelSize == messageTopLabelSize
- && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment
- && attributes.messageBottomLabelSize == messageBottomLabelSize
- && attributes.accessoryViewSize == accessoryViewSize
- && attributes.accessoryViewPadding == accessoryViewPadding
- && attributes.accessoryViewPosition == accessoryViewPosition
- && attributes.linkPreviewFonts == linkPreviewFonts
- } else {
- return false
- }
- }
+ public var messageTimeLabelSize: CGSize = .zero
+
+ public var accessoryViewSize: CGSize = .zero
+ public var accessoryViewPadding: HorizontalEdgeInsets = .zero
+ public var accessoryViewPosition: AccessoryPosition = .messageCenter
+
+ public var linkPreviewFonts = LinkPreviewFonts(
+ titleFont: .preferredFont(forTextStyle: .footnote),
+ teaserFont: .preferredFont(forTextStyle: .caption2),
+ domainFont: .preferredFont(forTextStyle: .caption1))
}
diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift
index c7e667137..85cb3890f 100644
--- a/Sources/Layout/TextMessageSizeCalculator.swift
+++ b/Sources/Layout/TextMessageSizeCalculator.swift
@@ -1,92 +1,95 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class TextMessageSizeCalculator: MessageSizeCalculator {
+ // MARK: Open
+
+ open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat {
+ let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath)
+ let textInsets = messageLabelInsets(for: message)
+ return maxWidth - textInsets.horizontal
+ }
+
+ open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize {
+ let maxWidth = messageContainerMaxWidth(for: message, at: indexPath)
+
+ var messageContainerSize: CGSize
+ let attributedText: NSAttributedString
+
+ let textMessageKind = message.kind.textMessageKind
+ switch textMessageKind {
+ case .attributedText(let text):
+ attributedText = text
+ case .text(let text), .emoji(let text):
+ attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont])
+ default:
+ fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+ }
- public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)
- public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)
+ messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
- public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+ let messageInsets = messageLabelInsets(for: message)
+ messageContainerSize.width += messageInsets.horizontal
+ messageContainerSize.height += messageInsets.vertical
- internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets {
- let dataSource = messagesLayout.messagesDataSource
- let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
- return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets
- }
+ return messageContainerSize
+ }
- open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat {
- let maxWidth = super.messageContainerMaxWidth(for: message)
- let textInsets = messageLabelInsets(for: message)
- return maxWidth - textInsets.horizontal
- }
+ open override func configure(attributes: UICollectionViewLayoutAttributes) {
+ super.configure(attributes: attributes)
+ guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
- open override func messageContainerSize(for message: MessageType) -> CGSize {
- let maxWidth = messageContainerMaxWidth(for: message)
+ let dataSource = messagesLayout.messagesDataSource
+ let indexPath = attributes.indexPath
+ let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
- var messageContainerSize: CGSize
- let attributedText: NSAttributedString
+ attributes.messageLabelInsets = messageLabelInsets(for: message)
+ attributes.messageLabelFont = messageLabelFont
- let textMessageKind = message.kind.textMessageKind
- switch textMessageKind {
- case .attributedText(let text):
- attributedText = text
- case .text(let text), .emoji(let text):
- attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont])
- default:
- fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
- }
+ switch message.kind {
+ case .attributedText(let text):
+ guard !text.string.isEmpty else { return }
+ guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return }
+ attributes.messageLabelFont = font
+ default:
+ break
+ }
+ }
- messageContainerSize = labelSize(for: attributedText, considering: maxWidth)
+ // MARK: Public
- let messageInsets = messageLabelInsets(for: message)
- messageContainerSize.width += messageInsets.horizontal
- messageContainerSize.height += messageInsets.vertical
+ public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)
+ public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)
- return messageContainerSize
- }
+ public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
- open override func configure(attributes: UICollectionViewLayoutAttributes) {
- super.configure(attributes: attributes)
- guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
-
- let dataSource = messagesLayout.messagesDataSource
- let indexPath = attributes.indexPath
- let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
-
- attributes.messageLabelInsets = messageLabelInsets(for: message)
- attributes.messageLabelFont = messageLabelFont
-
- switch message.kind {
- case .attributedText(let text):
- guard !text.string.isEmpty else { return }
- guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return }
- attributes.messageLabelFont = font
- default:
- break
- }
- }
+ // MARK: Internal
+
+ internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets {
+ let dataSource = messagesLayout.messagesDataSource
+ let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+ return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets
+ }
}
diff --git a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift
index 551465da9..f8d687956 100644
--- a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift
+++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift
@@ -1,38 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class TypingCellSizeCalculator: CellSizeCalculator {
+ // MARK: Lifecycle
+
+ public init(layout: MessagesCollectionViewFlowLayout? = nil) {
+ super.init()
+ self.layout = layout
+ }
- public init(layout: MessagesCollectionViewFlowLayout? = nil) {
- super.init()
- self.layout = layout
- }
+ // MARK: Open
- open override func sizeForItem(at indexPath: IndexPath) -> CGSize {
- guard let layout = layout as? MessagesCollectionViewFlowLayout else { return .zero }
- return layout.messagesLayoutDelegate.typingIndicatorViewSize(for: layout)
- }
+ open override func sizeForItem(at _: IndexPath) -> CGSize {
+ guard let layout = layout as? MessagesCollectionViewFlowLayout else { return .zero }
+ return layout.messagesLayoutDelegate.typingIndicatorViewSize(for: layout)
+ }
}
diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift
index cd2c486e9..59c09c5ce 100644
--- a/Sources/Models/AccessoryPosition.swift
+++ b/Sources/Models/AccessoryPosition.swift
@@ -1,48 +1,45 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// Used to determine the `Horizontal` and `Vertical` position of
/// an `AccessoryView` in a `MessageCollectionViewCell`.
-public enum AccessoryPosition {
-
- /// Aligns the `AccessoryView`'s top edge to the cell's top edge.
- case cellTop
-
- /// Aligns the `AccessoryView`'s top edge to the `messageTopLabel`'s top edge.
- case messageLabelTop
-
- /// Aligns the `AccessoryView`'s top edge to the `MessageContainerView`'s top edge.
- case messageTop
-
- /// Aligns the `AccessoryView` center to the `MessageContainerView` center.
- case messageCenter
-
- /// Aligns the `AccessoryView`'s bottom edge to the `MessageContainerView`s bottom edge.
- case messageBottom
-
- /// Aligns the `AccessoryView`'s bottom edge to the cell's bottom edge.
- case cellBottom
+public enum AccessoryPosition: Equatable {
+ /// Aligns the `AccessoryView`'s top edge to the cell's top edge.
+ case cellTop
+
+ /// Aligns the `AccessoryView`'s top edge to the `messageTopLabel`'s top edge.
+ case messageLabelTop
+
+ /// Aligns the `AccessoryView`'s top edge to the `MessageContainerView`'s top edge.
+ case messageTop
+
+ /// Aligns the `AccessoryView` center to the `MessageContainerView` center.
+ case messageCenter
+
+ /// Aligns the `AccessoryView`'s bottom edge to the `MessageContainerView`s bottom edge.
+ case messageBottom
+
+ /// Aligns the `AccessoryView`'s bottom edge to the cell's bottom edge.
+ case cellBottom
}
diff --git a/Sources/Models/Avatar.swift b/Sources/Models/Avatar.swift
index 5c7bc47d9..42872a755 100644
--- a/Sources/Models/Avatar.swift
+++ b/Sources/Models/Avatar.swift
@@ -1,48 +1,44 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
/// An object used to group the information to be used by an `AvatarView`.
public struct Avatar {
-
- // MARK: - Properties
-
- /// The image to be used for an `AvatarView`.
- public let image: UIImage?
-
- /// The placeholder initials to be used in the case where no image is provided.
- ///
- /// The default value of this property is "?".
- public var initials: String = "?"
-
- // MARK: - Initializer
-
- public init(image: UIImage? = nil, initials: String = "?") {
- self.image = image
- self.initials = initials
- }
-
+ // MARK: - Properties
+
+ /// The image to be used for an `AvatarView`.
+ public let image: UIImage?
+
+ /// The placeholder initials to be used in the case where no image is provided.
+ ///
+ /// The default value of this property is "?".
+ public var initials = "?"
+
+ // MARK: - Initializer
+
+ public init(image: UIImage? = nil, initials: String = "?") {
+ self.image = image
+ self.initials = initials
+ }
}
diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift
index f3c11e954..9f6000a26 100644
--- a/Sources/Models/AvatarPosition.swift
+++ b/Sources/Models/AvatarPosition.swift
@@ -1,99 +1,92 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
+// MARK: - AvatarPosition
+
/// Used to determine the `Horizontal` and `Vertical` position of
// an `AvatarView` in a `MessageCollectionViewCell`.
public struct AvatarPosition: Equatable {
-
- /// An enum representing the horizontal alignment of an `AvatarView`.
- public enum Horizontal {
-
- /// Positions the `AvatarView` on the side closest to the cell's leading edge.
- case cellLeading
-
- /// Positions the `AvatarView` on the side closest to the cell's trailing edge.
- case cellTrailing
-
- /// Positions the `AvatarView` based on whether the message is from the current Sender.
- /// The cell is positioned `.cellTrailling` if `isFromCurrentSender` is true
- /// and `.cellLeading` if false.
- case natural
- }
-
- /// An enum representing the verical alignment for an `AvatarView`.
- public enum Vertical {
-
- /// Aligns the `AvatarView`'s top edge to the cell's top edge.
- case cellTop
-
- /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge.
- case messageLabelTop
-
- /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
- case messageTop
-
- /// Aligns the `AvatarView` center to the `MessageContainerView` center.
- case messageCenter
-
- /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
- case messageBottom
-
- /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
- case cellBottom
-
- }
-
- // MARK: - Properties
-
- // The vertical position
- public var vertical: Vertical
-
- // The horizontal position
- public var horizontal: Horizontal
-
- // MARK: - Initializers
-
- public init(horizontal: Horizontal, vertical: Vertical) {
- self.horizontal = horizontal
- self.vertical = vertical
- }
-
- public init(vertical: Vertical) {
- self.init(horizontal: .natural, vertical: vertical)
- }
-
+ // MARK: Lifecycle
+
+ public init(horizontal: Horizontal, vertical: Vertical) {
+ self.horizontal = horizontal
+ self.vertical = vertical
+ }
+
+ public init(vertical: Vertical) {
+ self.init(horizontal: .natural, vertical: vertical)
+ }
+
+ // MARK: Public
+
+ /// An enum representing the horizontal alignment of an `AvatarView`.
+ public enum Horizontal {
+ /// Positions the `AvatarView` on the side closest to the cell's leading edge.
+ case cellLeading
+
+ /// Positions the `AvatarView` on the side closest to the cell's trailing edge.
+ case cellTrailing
+
+ /// Positions the `AvatarView` based on whether the message is from the current Sender.
+ /// The cell is positioned `.cellTrailing` if `isFromCurrentSender` is true
+ /// and `.cellLeading` if false.
+ case natural
+ }
+
+ /// An enum representing the vertical alignment for an `AvatarView`.
+ public enum Vertical {
+ /// Aligns the `AvatarView`'s top edge to the cell's top edge.
+ case cellTop
+
+ /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge.
+ case messageLabelTop
+
+ /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
+ case messageTop
+
+ /// Aligns the `AvatarView` center to the `MessageContainerView` center.
+ case messageCenter
+
+ /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
+ case messageBottom
+
+ /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
+ case cellBottom
+ }
+
+ // The vertical position
+ public var vertical: Vertical
+
+ // The horizontal position
+ public var horizontal: Horizontal
}
// MARK: - Equatable Conformance
-public extension AvatarPosition {
-
- static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool {
- return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
- }
-
+extension AvatarPosition {
+ public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool {
+ lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
+ }
}
diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift
index 1b711a779..7fd0cb228 100644
--- a/Sources/Models/DetectorType.swift
+++ b/Sources/Models/DetectorType.swift
@@ -1,77 +1,80 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2019 MessageKit
+import Foundation
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+public enum DetectorType: Hashable, Sendable {
+ case address
+ case date
+ case phoneNumber
+ case url
+ case transitInformation
+ case custom(NSRegularExpression)
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ // MARK: Public
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ // swiftlint:disable force_try
+ public static let hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: []))
+ public static let mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: []))
-import Foundation
+ /// Simply check if the detector type is a .custom
+ public var isCustom: Bool {
+ switch self {
+ case .custom: return true
+ default: return false
+ }
+ }
-public enum DetectorType: Hashable {
+ /// The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted.
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(toInt())
+ }
- case address
- case date
- case phoneNumber
- case url
- case transitInformation
- case custom(NSRegularExpression)
+ // MARK: Internal
- // swiftlint:disable force_try
- public static var hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: []))
- public static var mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: []))
- // swiftlint:enable force_try
+ // swiftlint:enable force_try
- internal var textCheckingType: NSTextCheckingResult.CheckingType {
- switch self {
- case .address: return .address
- case .date: return .date
- case .phoneNumber: return .phoneNumber
- case .url: return .link
- case .transitInformation: return .transitInformation
- case .custom: return .regularExpression
- }
+ internal var textCheckingType: NSTextCheckingResult.CheckingType {
+ switch self {
+ case .address: return .address
+ case .date: return .date
+ case .phoneNumber: return .phoneNumber
+ case .url: return .link
+ case .transitInformation: return .transitInformation
+ case .custom: return .regularExpression
}
+ }
- /// Simply check if the detector type is a .custom
- public var isCustom: Bool {
- switch self {
- case .custom: return true
- default: return false
- }
- }
+ // MARK: Private
- ///The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted.
- public func hash(into hasher: inout Hasher) {
- hasher.combine(toInt())
+ /// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable`
+ private func toInt() -> Int {
+ switch self {
+ case .address: return 0
+ case .date: return 1
+ case .phoneNumber: return 2
+ case .url: return 3
+ case .transitInformation: return 4
+ case .custom(let regex): return regex.hashValue
}
-
- /// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable`
- private func toInt() -> Int {
- switch self {
- case .address: return 0
- case .date: return 1
- case .phoneNumber: return 2
- case .url: return 3
- case .transitInformation: return 4
- case .custom(let regex): return regex.hashValue
- }
- }
-
+ }
}
diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift
index 695f4e506..86cfc3f2d 100644
--- a/Sources/Models/HorizontalEdgeInsets.swift
+++ b/Sources/Models/HorizontalEdgeInsets.swift
@@ -1,58 +1,55 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
-/// A varient of `UIEdgeInsets` that only has horizontal inset properties
-public struct HorizontalEdgeInsets: Equatable {
+// MARK: - HorizontalEdgeInsets
- public var left: CGFloat
- public var right: CGFloat
+/// A variant of `UIEdgeInsets` that only has horizontal inset properties
+public struct HorizontalEdgeInsets: Equatable {
+ public var left: CGFloat
+ public var right: CGFloat
- public init(left: CGFloat, right: CGFloat) {
- self.left = left
- self.right = right
- }
+ public init(left: CGFloat, right: CGFloat) {
+ self.left = left
+ self.right = right
+ }
- public static var zero: HorizontalEdgeInsets {
- return HorizontalEdgeInsets(left: 0, right: 0)
- }
+ public static var zero: HorizontalEdgeInsets {
+ HorizontalEdgeInsets(left: 0, right: 0)
+ }
}
// MARK: Equatable Conformance
-public extension HorizontalEdgeInsets {
-
- static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool {
- return lhs.left == rhs.left && lhs.right == rhs.right
- }
+extension HorizontalEdgeInsets {
+ public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool {
+ lhs.left == rhs.left && lhs.right == rhs.right
+ }
}
-internal extension HorizontalEdgeInsets {
-
- var horizontal: CGFloat {
- return left + right
- }
+extension HorizontalEdgeInsets {
+ internal var horizontal: CGFloat {
+ left + right
+ }
}
diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift
index 314650750..fc9616dcf 100644
--- a/Sources/Models/LabelAlignment.swift
+++ b/Sources/Models/LabelAlignment.swift
@@ -1,47 +1,43 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
-public struct LabelAlignment: Equatable {
+// MARK: - LabelAlignment
- public var textAlignment: NSTextAlignment
- public var textInsets: UIEdgeInsets
-
- public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) {
- self.textAlignment = textAlignment
- self.textInsets = textInsets
- }
+public struct LabelAlignment: Equatable {
+ public var textAlignment: NSTextAlignment
+ public var textInsets: UIEdgeInsets
+ public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) {
+ self.textAlignment = textAlignment
+ self.textInsets = textInsets
+ }
}
// MARK: - Equatable Conformance
-public extension LabelAlignment {
-
- static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool {
- return lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets
- }
-
+extension LabelAlignment {
+ public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool {
+ lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets
+ }
}
diff --git a/Sources/Models/LinkPreviewFonts.swift b/Sources/Models/LinkPreviewFonts.swift
index 89936a81e..8d6c14733 100644
--- a/Sources/Models/LinkPreviewFonts.swift
+++ b/Sources/Models/LinkPreviewFonts.swift
@@ -1,32 +1,30 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
public struct LinkPreviewFonts: Equatable {
- let titleFont: UIFont
- let teaserFont: UIFont
- let domainFont: UIFont
+ let titleFont: UIFont
+ let teaserFont: UIFont
+ let domainFont: UIFont
}
diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift
index ccef50c51..792a07310 100644
--- a/Sources/Models/LocationMessageSnapshotOptions.swift
+++ b/Sources/Models/LocationMessageSnapshotOptions.swift
@@ -1,64 +1,70 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2019 MessageKit
+import MapKit
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`.
+@MainActor
+public struct LocationMessageSnapshotOptions {
+ // MARK: Lifecycle
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ /// Initialize LocationMessageSnapshotOptions with given parameters
+ ///
+ /// - Parameters:
+ /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings.
+ /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest.
+ /// - span: The span of the snapshot.
+ /// - scale: The scale of the snapshot.
+ public init(
+ showsBuildings: Bool = false,
+ showsPointsOfInterest: Bool = false,
+ span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0),
+ scale: CGFloat = UIScreen.main.scale)
+ {
+ self.showsBuildings = showsBuildings
+ self.showsPointsOfInterest = showsPointsOfInterest
+ self.span = span
+ self.scale = scale
+ }
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ // MARK: Public
-import MapKit
+ /// A Boolean value indicating whether the snapshot image should display buildings.
+ ///
+ /// The default value of this property is `false`.
+ public var showsBuildings: Bool
-/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`.
-public struct LocationMessageSnapshotOptions {
+ /// A Boolean value indicating whether the snapshot image should display points of interest.
+ ///
+ /// The default value of this property is `false`.
+ public var showsPointsOfInterest: Bool
+
+ /// The span of the snapshot.
+ ///
+ /// The default value of this property uses a width of `0` and height of `0`.
+ public var span: MKCoordinateSpan
- /// Initialize LocationMessageSnapshotOptions with given parameters
- ///
- /// - Parameters:
- /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings.
- /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest.
- /// - span: The span of the snapshot.
- /// - scale: The scale of the snapshot.
- public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), scale: CGFloat = UIScreen.main.scale) {
- self.showsBuildings = showsBuildings
- self.showsPointsOfInterest = showsPointsOfInterest
- self.span = span
- self.scale = scale
- }
-
- /// A Boolean value indicating whether the snapshot image should display buildings.
- ///
- /// The default value of this property is `false`.
- public var showsBuildings: Bool
-
- /// A Boolean value indicating whether the snapshot image should display points of interest.
- ///
- /// The default value of this property is `false`.
- public var showsPointsOfInterest: Bool
-
- /// The span of the snapshot.
- ///
- /// The default value of this property uses a width of `0` and height of `0`.
- public var span: MKCoordinateSpan
-
- /// The scale of the snapshot.
- ///
- /// The default value of this property uses the `UIScreen.main.scale`.
- public var scale: CGFloat
-
+ /// The scale of the snapshot.
+ ///
+ /// The default value of this property uses the `UIScreen.main.scale`.
+ public var scale: CGFloat
}
diff --git a/Sources/Models/MessageInputBarKind.swift b/Sources/Models/MessageInputBarKind.swift
new file mode 100644
index 000000000..6a127b01c
--- /dev/null
+++ b/Sources/Models/MessageInputBarKind.swift
@@ -0,0 +1,29 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+public enum MessageInputBarKind: Equatable {
+ case messageInputBar
+ case custom(UIView)
+}
diff --git a/Sources/Models/MessageKind.swift b/Sources/Models/MessageKind.swift
index b8dbea23d..cb11f2d12 100644
--- a/Sources/Models/MessageKind.swift
+++ b/Sources/Models/MessageKind.swift
@@ -1,75 +1,71 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// An enum representing the kind of message and its underlying kind.
public enum MessageKind {
+ /// A standard text message.
+ ///
+ /// - Note: The font used for this message will be the value of the
+ /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object.
+ ///
+ /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you
+ /// to set this property and results in higher performance.
+ case text(String)
- /// A standard text message.
- ///
- /// - Note: The font used for this message will be the value of the
- /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object.
- ///
- /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you
- /// to set this property and results in higher performance.
- case text(String)
-
- /// A message with attributed text.
- case attributedText(NSAttributedString)
+ /// A message with attributed text.
+ case attributedText(NSAttributedString)
- /// A photo message.
- case photo(MediaItem)
+ /// A photo message.
+ case photo(MediaItem)
- /// A video message.
- case video(MediaItem)
+ /// A video message.
+ case video(MediaItem)
- /// A location message.
- case location(LocationItem)
+ /// A location message.
+ case location(LocationItem)
- /// An emoji message.
- case emoji(String)
+ /// An emoji message.
+ case emoji(String)
- /// An audio message.
- case audio(AudioItem)
-
- /// A contact message.
- case contact(ContactItem)
+ /// An audio message.
+ case audio(AudioItem)
- /// A link preview message.
- case linkPreview(LinkItem)
+ /// A contact message.
+ case contact(ContactItem)
- /// A custom message.
- /// - Note: Using this case requires that you implement the following methods and handle this case:
- /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
- /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
- case custom(Any?)
+ /// A link preview message.
+ case linkPreview(LinkItem)
- // MARK: - Not supported yet
+ /// A custom message.
+ /// - Note: Using this case requires that you implement the following methods and handle this case:
+ /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
+ /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
+ case custom(Any?)
+
+ // MARK: - Not supported yet
// case system(String)
-//
+//
// case placeholder
-
}
diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift
index 76e1f94e6..c3636034e 100644
--- a/Sources/Models/MessageKitDateFormatter.swift
+++ b/Sources/Models/MessageKitDateFormatter.swift
@@ -1,66 +1,70 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2019 MessageKit
+import Foundation
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+open class MessageKitDateFormatter: @unchecked Sendable {
+ // MARK: Lifecycle
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+ // MARK: - Initializer
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+ private init() { }
-import Foundation
+ // MARK: Open
-open class MessageKitDateFormatter {
+ open func configureDateFormatter(for date: Date) {
+ switch true {
+ case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date):
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear):
+ formatter.dateFormat = "EEEE h:mm a"
+ case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year):
+ formatter.dateFormat = "E, d MMM, h:mm a"
+ default:
+ formatter.dateFormat = "MMM d, yyyy, h:mm a"
+ }
+ }
- // MARK: - Properties
+ // MARK: Public
- public static let shared = MessageKitDateFormatter()
+ // MARK: - Properties
- private let formatter = DateFormatter()
+ public static let shared = MessageKitDateFormatter()
- // MARK: - Initializer
+ // MARK: - Methods
- private init() {}
+ public func string(from date: Date) -> String {
+ configureDateFormatter(for: date)
+ return formatter.string(from: date)
+ }
- // MARK: - Methods
+ public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
+ let dateString = string(from: date)
+ return NSAttributedString(string: dateString, attributes: attributes)
+ }
- public func string(from date: Date) -> String {
- configureDateFormatter(for: date)
- return formatter.string(from: date)
- }
+ // MARK: Private
- public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
- let dateString = string(from: date)
- return NSAttributedString(string: dateString, attributes: attributes)
- }
-
- open func configureDateFormatter(for date: Date) {
- switch true {
- case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date):
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
- case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear):
- formatter.dateFormat = "EEEE h:mm a"
- case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year):
- formatter.dateFormat = "E, d MMM, h:mm a"
- default:
- formatter.dateFormat = "MMM d, yyyy, h:mm a"
- }
- }
-
+ private let formatter = DateFormatter()
}
diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift
index 53c2d025e..2ac4ca46b 100644
--- a/Sources/Models/MessageKitError.swift
+++ b/Sources/Models/MessageKitError.swift
@@ -1,38 +1,36 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-internal struct MessageKitError {
- static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved."
- static let nilMessagesDataSource = "MessagesDataSource has not been set."
- static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set."
- static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set."
- static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView."
- static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type."
- static let unrecognizedSectionKind = "Received unrecognized element kind:"
- static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType"
- static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle"
- static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)."
- static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)."
- static let couldNotFindColorAsset = "MessageKit: Could not load the color asset."
+internal enum MessageKitError {
+ static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved."
+ static let nilMessagesDataSource = "MessagesDataSource has not been set."
+ static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set."
+ static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set."
+ static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView."
+ static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type."
+ static let unrecognizedSectionKind = "Received unrecognized element kind:"
+ static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType"
+ static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle"
+ static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)."
+ static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)."
+ static let couldNotFindColorAsset = "MessageKit: Could not load the color asset."
}
diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift
index c5d6a3275..ed1dbeba4 100644
--- a/Sources/Models/MessageStyle.swift
+++ b/Sources/Models/MessageStyle.swift
@@ -1,148 +1,157 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
public enum MessageStyle {
+ // MARK: - MessageStyle
- // MARK: - TailCorner
+ case none
+ case bubble
+ case bubbleOutline(UIColor)
+ case bubbleTail(TailCorner, TailStyle)
+ case bubbleTailOutline(UIColor, TailCorner, TailStyle)
+ case customImageTail(UIImage,TailCorner)
+ case custom((MessageContainerView) -> Void)
- public enum TailCorner: String {
+ // MARK: Public
- case topLeft
- case bottomLeft
- case topRight
- case bottomRight
+ // MARK: - TailCorner
- internal var imageOrientation: UIImage.Orientation {
- switch self {
+ public enum TailCorner: String {
+ case topLeft
+ case bottomLeft
+ case topRight
+ case bottomRight
+
+ internal var imageOrientation: UIImage.Orientation {
+ switch self {
case .bottomRight: return .up
case .bottomLeft: return .upMirrored
case .topLeft: return .down
case .topRight: return .downMirrored
- }
}
}
+ }
- // MARK: - TailStyle
+ // MARK: - TailStyle
- public enum TailStyle {
+ public enum TailStyle {
+ case curved
+ case pointedEdge
- case curved
- case pointedEdge
-
- internal var imageNameSuffix: String {
- switch self {
- case .curved:
- return "_tail_v2"
- case .pointedEdge:
- return "_tail_v1"
- }
- }
+ internal var imageNameSuffix: String {
+ switch self {
+ case .curved:
+ return "_tail_v2"
+ case .pointedEdge:
+ return "_tail_v1"
+ }
+ }
+ }
+
+ public var image: UIImage? {
+ if
+ let imageCacheKey = imageCacheKey,
+ let cachedImage = MessageStyle.bubbleImageCache.object(forKey: imageCacheKey as NSString)
+ {
+ 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)
}
- // MARK: - MessageStyle
-
- case none
- case bubble
- case bubbleOutline(UIColor)
- case bubbleTail(TailCorner, TailStyle)
- case bubbleTailOutline(UIColor, TailCorner, TailStyle)
- case custom((MessageContainerView) -> Void)
-
- // MARK: - Public
+ guard
+ let imageName = imageName,
+ var image = UIImage(named: imageName, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
+ else {
+ return nil
+ }
- public var image: UIImage? {
- if let imageCacheKey = imageCacheKey, let cachedImage = MessageStyle.bubbleImageCache.object(forKey: imageCacheKey as NSString) {
- return cachedImage
- }
+ switch self {
+ case .none, .custom:
+ return nil
+ 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)
+ }
- guard
- let imageName = imageName,
- var image = UIImage(named: imageName, in: Bundle.messageKitAssetBundle, compatibleWith: nil)
- else {
- return nil
- }
+ return strechAndCache(image: image)
+ }
- switch self {
- case .none, .custom:
- return nil
- case .bubble, .bubbleOutline:
- 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
- }
+ // MARK: Internal
- // MARK: - Internal
-
- internal static let bubbleImageCache: NSCache = {
+ nonisolated(unsafe) internal static let bubbleImageCache: NSCache = {
let cache = NSCache()
cache.name = "com.messagekit.MessageKit.bubbleImageCache"
return cache
}()
-
- // MARK: - Private
-
- private var imageCacheKey: String? {
- guard let imageName = imageName else { return nil }
-
- switch self {
- case .bubble, .bubbleOutline:
- return imageName
- case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
- return imageName + "_" + corner.rawValue
- default:
- return nil
- }
- }
- private var imageName: String? {
- switch self {
- case .bubble:
- return "bubble_full"
- case .bubbleOutline:
- return "bubble_outlined"
- case .bubbleTail(_, let tailStyle):
- return "bubble_full" + tailStyle.imageNameSuffix
- case .bubbleTailOutline(_, _, let tailStyle):
- return "bubble_outlined" + tailStyle.imageNameSuffix
- case .none, .custom:
- return nil
- }
- }
+ // MARK: Private
- private func stretch(_ image: UIImage) -> UIImage {
- let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
- let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
- return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
+ private var imageCacheKey: String? {
+ guard let imageName = imageName else { return nil }
+
+ switch self {
+ case .bubble, .bubbleOutline:
+ return imageName
+ case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _):
+ return imageName + "_" + corner.rawValue
+ default:
+ return nil
+ }
+ }
+
+ private var imageName: String? {
+ switch self {
+ case .bubble:
+ return "bubble_full"
+ case .bubbleOutline:
+ return "bubble_outlined"
+ case .bubbleTail(_, let tailStyle):
+ return "bubble_full" + tailStyle.imageNameSuffix
+ case .bubbleTailOutline(_, _, let tailStyle):
+ return "bubble_outlined" + tailStyle.imageNameSuffix
+ case .none, .custom, .customImageTail:
+ return nil
}
+ }
+
+ private func stretch(_ image: UIImage) -> UIImage {
+ let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
+ let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
+ return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
+ }
}
diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift
deleted file mode 100644
index 791409285..000000000
--- a/Sources/Models/NSConstraintLayoutSet.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import UIKit
-
-internal class NSLayoutConstraintSet {
-
- internal var top: NSLayoutConstraint?
- internal var bottom: NSLayoutConstraint?
- internal var left: NSLayoutConstraint?
- internal var right: NSLayoutConstraint?
- internal var centerX: NSLayoutConstraint?
- internal var centerY: NSLayoutConstraint?
- internal var width: NSLayoutConstraint?
- internal var height: NSLayoutConstraint?
-
- internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil,
- left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil,
- centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil,
- width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) {
- self.top = top
- self.bottom = bottom
- self.left = left
- self.right = right
- self.centerX = centerX
- self.centerY = centerY
- self.width = width
- self.height = height
- }
-
- /// All of the currently configured constraints
- private var availableConstraints: [NSLayoutConstraint] {
- let constraints = [top, bottom, left, right, centerX, centerY, width, height]
- var available: [NSLayoutConstraint] = []
- for constraint in constraints {
- if let value = constraint {
- available.append(value)
- }
- }
- return available
- }
-
- /// Activates all of the non-nil constraints
- ///
- /// - Returns: Self
- @discardableResult
- internal func activate() -> Self {
- NSLayoutConstraint.activate(availableConstraints)
- return self
- }
-
- /// Deactivates all of the non-nil constraints
- ///
- /// - Returns: Self
- @discardableResult
- internal func deactivate() -> Self {
- NSLayoutConstraint.deactivate(availableConstraints)
- return self
- }
-}
diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift
deleted file mode 100644
index c44b8be16..000000000
--- a/Sources/Models/Sender.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
-
-/// An object that groups the metadata of a messages sender.
-@available(*, deprecated, message: "`Sender` has been replaced with the `SenderType` protocol in 3.0.0")
-public struct Sender: SenderType {
-
- // MARK: - Properties
-
- /// The unique String identifier for the sender.
- ///
- /// Note: This value must be unique across all senders.
- public let senderId: String
-
- @available(*, deprecated, renamed: "senderId", message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol")
- public var id: String {
- return senderId
- }
-
- /// The display name of a sender.
- public let displayName: String
-
- // MARK: - Intializers
-
- public init(senderId: String, displayName: String) {
- self.senderId = senderId
- self.displayName = displayName
- }
-
- @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/AudioItem.swift b/Sources/Protocols/AudioItem.swift
index cae71fdaa..db24e7c58 100644
--- a/Sources/Protocols/AudioItem.swift
+++ b/Sources/Protocols/AudioItem.swift
@@ -1,41 +1,37 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+import class AVFoundation.AVAudioPlayer
import Foundation
import UIKit
-import class AVFoundation.AVAudioPlayer
/// A protocol used to represent the data for an audio message.
public protocol AudioItem {
+ /// The url where the audio file is located.
+ var url: URL { get }
- /// The url where the audio file is located.
- var url: URL { get }
-
- /// The audio file duration in seconds.
- var duration: Float { get }
-
- /// The size of the audio item.
- var size: CGSize { get }
+ /// The audio file duration in seconds.
+ var duration: Float { get }
+ /// The size of the audio item.
+ var size: CGSize { get }
}
diff --git a/Sources/Protocols/ContactItem.swift b/Sources/Protocols/ContactItem.swift
index f43e842d9..9a40fc78d 100644
--- a/Sources/Protocols/ContactItem.swift
+++ b/Sources/Protocols/ContactItem.swift
@@ -1,41 +1,38 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// A protocol used to represent the data for a contact message.
public protocol ContactItem {
-
- /// contact displayed name
- var displayName: String { get }
-
- /// initials from contact first and last name
- var initials: String { get }
-
- /// contact phone numbers
- var phoneNumbers: [String] { get }
-
- /// contact emails
- var emails: [String] { get }
+ /// contact displayed name
+ var displayName: String { get }
+
+ /// initials from contact first and last name
+ var initials: String { get }
+
+ /// contact phone numbers
+ var phoneNumbers: [String] { get }
+
+ /// contact emails
+ var emails: [String] { get }
}
diff --git a/Sources/Protocols/LinkItem.swift b/Sources/Protocols/LinkItem.swift
index d8b16a388..5d3342a6f 100644
--- a/Sources/Protocols/LinkItem.swift
+++ b/Sources/Protocols/LinkItem.swift
@@ -1,67 +1,66 @@
-/*
- MIT License
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
import CoreGraphics
+import Foundation
import UIKit
+// MARK: - LinkItem
+
/// A protocol used to represent the data for a link preview message.
public protocol LinkItem {
+ /// A link item needs a message to present, it can be a simple String or
+ /// a NSAttributedString, but only one will be shown.
+ /// LinkItem.text has priority over LinkItem.attributedText.
- /// A link item needs a message to present, it can be a simple String or
- /// a NSAttributedString, but only one will be shown.
- /// LinkItem.text has priority over LinkeItem.attributedText.
-
- /// The message text.
- var text: String? { get }
+ /// The message text.
+ var text: String? { get }
- /// The message attributed text.
- var attributedText: NSAttributedString? { get }
+ /// The message attributed text.
+ var attributedText: NSAttributedString? { get }
- /// The URL.
- var url: URL { get }
+ /// The URL.
+ var url: URL { get }
- /// The title.
- var title: String? { get }
+ /// The title.
+ var title: String? { get }
- /// The teaser text.
- var teaser: String { get }
+ /// The teaser text.
+ var teaser: String { get }
- /// The thumbnail image.
- var thumbnailImage: UIImage { get }
+ /// The thumbnail image.
+ var thumbnailImage: UIImage { get }
}
-public extension LinkItem {
- var textKind: MessageKind {
- let kind: MessageKind
- if let text = self.text {
- kind = .text(text)
- } else if let attributedText = self.attributedText {
- kind = .attributedText(attributedText)
- } else {
- fatalError("LinkItem must have \"text\" or \"attributedText\"")
- }
- return kind
+extension LinkItem {
+ public var textKind: MessageKind {
+ let kind: MessageKind
+ if let text = text {
+ kind = .text(text)
+ } else if let attributedText = attributedText {
+ kind = .attributedText(attributedText)
+ } else {
+ fatalError("LinkItem must have \"text\" or \"attributedText\"")
}
+ return kind
+ }
}
diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift
index 10ba2c0f5..8789bfa8a 100644
--- a/Sources/Protocols/LocationItem.swift
+++ b/Sources/Protocols/LocationItem.swift
@@ -1,38 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+import class CoreLocation.CLLocation
import Foundation
import UIKit
-import class CoreLocation.CLLocation
/// A protocol used to represent the data for a location message.
public protocol LocationItem {
+ /// The location.
+ var location: CLLocation { get }
- /// The location.
- var location: CLLocation { get }
-
- /// The size of the location item.
- var size: CGSize { get }
-
+ /// The size of the location item.
+ var size: CGSize { get }
}
diff --git a/Sources/Protocols/MediaItem.swift b/Sources/Protocols/MediaItem.swift
index 86525c3e4..28160d4b8 100644
--- a/Sources/Protocols/MediaItem.swift
+++ b/Sources/Protocols/MediaItem.swift
@@ -1,43 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
/// A protocol used to represent the data for a media message.
public protocol MediaItem {
+ /// The url where the media is located.
+ var url: URL? { get }
- /// The url where the media is located.
- var url: URL? { get }
-
- /// The image.
- var image: UIImage? { get }
-
- /// A placeholder image for when the image is obtained asychronously.
- var placeholderImage: UIImage { get }
+ /// The image.
+ var image: UIImage? { get }
- /// The size of the media item.
- var size: CGSize { get }
+ /// A placeholder image for when the image is obtained asynchronously.
+ var placeholderImage: UIImage { get }
+ /// The size of the media item.
+ var size: CGSize { get }
}
diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift
index db2d71189..a243a773d 100644
--- a/Sources/Protocols/MessageCellDelegate.swift
+++ b/Sources/Protocols/MessageCellDelegate.swift
@@ -1,192 +1,188 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+// MARK: - MessageCellDelegate
+
/// A protocol used by `MessageContentCell` subclasses to detect taps in the cell's subviews.
public protocol MessageCellDelegate: MessageLabelDelegate {
-
- /// Triggered when a tap occurs in the background of the cell.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// - Note:
- /// 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 didTapBackground(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the `MessageContainerView`.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap occurred.
- ///
- /// - Note:
- /// 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 didTapMessage(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the `AvatarView`.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap 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 didTapAvatar(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the cellTopLabel.
- ///
- /// - Parameters:
- /// - cell: The cell tap 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 didTapCellTopLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the cellBottomLabel.
- ///
- /// - Parameters:
- /// - cell: The cell tap 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 didTapCellBottomLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the messageTopLabel.
- ///
- /// - Parameters:
- /// - cell: The cell tap 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 didTapMessageTopLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the messageBottomLabel.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap 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 didTapMessageBottomLabel(in cell: MessageCollectionViewCell)
-
- /// Triggered when a tap occurs in the accessoryView.
- ///
- /// - Parameters:
- /// - cell: The cell where the tap 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 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:
- /// - cell: The audio cell 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 didTapPlayButton(in cell: AudioMessageCell)
-
- /// Triggered when audio player start playing audio.
- ///
- /// - Parameters:
- /// - cell: The cell where the audio sound is playing.
- ///
- /// 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 didStartAudio(in cell: AudioMessageCell)
-
- /// Triggered when audio player pause audio.
- ///
- /// - Parameters:
- /// - cell: The cell where the audio sound is paused.
- ///
- /// 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 didPauseAudio(in cell: AudioMessageCell)
-
- /// Triggered when audio player stoped audio.
- ///
- /// - Parameters:
- /// - cell: The cell where the audio sound is stoped.
- ///
- /// 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 didStopAudio(in cell: AudioMessageCell)
-
+ /// Triggered when a tap occurs in the background of the cell.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// - Note:
+ /// 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 didTapBackground(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap occurred.
+ ///
+ /// - Note:
+ /// 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 didTapMessage(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the `AvatarView`.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap 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 didTapAvatar(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the cellTopLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap 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 didTapCellTopLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the cellBottomLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap 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 didTapCellBottomLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the messageTopLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell tap 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 didTapMessageTopLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the messageBottomLabel.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap 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 didTapMessageBottomLabel(in cell: MessageCollectionViewCell)
+
+ /// Triggered when a tap occurs in the accessoryView.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the tap 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 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:
+ /// - cell: The audio cell 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 didTapPlayButton(in cell: AudioMessageCell)
+
+ /// Triggered when audio player start playing audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is playing.
+ ///
+ /// 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 didStartAudio(in cell: AudioMessageCell)
+
+ /// Triggered when audio player pause audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is paused.
+ ///
+ /// 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 didPauseAudio(in cell: AudioMessageCell)
+
+ /// Triggered when audio player stoped audio.
+ ///
+ /// - Parameters:
+ /// - cell: The cell where the audio sound is stoped.
+ ///
+ /// 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 didStopAudio(in cell: AudioMessageCell)
}
-public extension MessageCellDelegate {
+extension MessageCellDelegate {
+ public func didTapBackground(in _: MessageCollectionViewCell) { }
+
+ public func didTapMessage(in _: MessageCollectionViewCell) { }
- func didTapBackground(in cell: MessageCollectionViewCell) {}
+ public func didTapAvatar(in _: MessageCollectionViewCell) { }
- func didTapMessage(in cell: MessageCollectionViewCell) {}
+ public func didTapCellTopLabel(in _: MessageCollectionViewCell) { }
- func didTapAvatar(in cell: MessageCollectionViewCell) {}
+ public func didTapCellBottomLabel(in _: MessageCollectionViewCell) { }
- func didTapCellTopLabel(in cell: MessageCollectionViewCell) {}
-
- func didTapCellBottomLabel(in cell: MessageCollectionViewCell) {}
+ public func didTapMessageTopLabel(in _: MessageCollectionViewCell) { }
- func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {}
-
- func didTapImage(in cell: MessageCollectionViewCell) {}
+ public func didTapImage(in _: MessageCollectionViewCell) { }
- func didTapPlayButton(in cell: AudioMessageCell) {}
+ public func didTapPlayButton(in _: AudioMessageCell) { }
- func didStartAudio(in cell: AudioMessageCell) {}
+ public func didStartAudio(in _: AudioMessageCell) { }
- func didPauseAudio(in cell: AudioMessageCell) {}
+ public func didPauseAudio(in _: AudioMessageCell) { }
- func didStopAudio(in cell: AudioMessageCell) {}
+ public func didStopAudio(in _: AudioMessageCell) { }
- func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {}
-
- func didTapAccessoryView(in cell: MessageCollectionViewCell) {}
+ public func didTapMessageBottomLabel(in _: MessageCollectionViewCell) { }
+ public func didTapAccessoryView(in _: MessageCollectionViewCell) { }
}
diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift
index bdd4ee66e..a1b7da6e5 100644
--- a/Sources/Protocols/MessageLabelDelegate.swift
+++ b/Sources/Protocols/MessageLabelDelegate.swift
@@ -1,99 +1,95 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
+// MARK: - MessageLabelDelegate
+
/// A protocol used to handle tap events on detected text.
public protocol MessageLabelDelegate: AnyObject {
-
- /// Triggered when a tap occurs on a detected address.
- ///
- /// - Parameters:
- /// - addressComponents: The components of the selected address.
- func didSelectAddress(_ addressComponents: [String: String])
-
- /// Triggered when a tap occurs on a detected date.
- ///
- /// - Parameters:
- /// - date: The selected date.
- func didSelectDate(_ date: Date)
-
- /// Triggered when a tap occurs on a detected phone number.
- ///
- /// - Parameters:
- /// - phoneNumber: The selected phone number.
- func didSelectPhoneNumber(_ phoneNumber: String)
-
- /// Triggered when a tap occurs on a detected URL.
- ///
- /// - Parameters:
- /// - url: The selected URL.
- func didSelectURL(_ url: URL)
-
- /// Triggered when a tap occurs on detected transit information.
- ///
- /// - Parameters:
- /// - transitInformation: The selected transit information.
- func didSelectTransitInformation(_ transitInformation: [String: String])
-
- /// Triggered when a tap occurs on a mention
- ///
- /// - Parameters:
- /// - mention: The selected mention
- func didSelectMention(_ mention: String)
-
- /// Triggered when a tap occurs on a hashtag
- ///
- /// - Parameters:
- /// - mention: The selected hashtag
- func didSelectHashtag(_ hashtag: String)
-
- /// Triggered when a tap occurs on a custom regular expression
- ///
- /// - Parameters:
- /// - pattern: the pattern of the regular expression
- /// - match: part that match with the regular expression
- func didSelectCustom(_ pattern: String, match: String?)
-
+ /// Triggered when a tap occurs on a detected address.
+ ///
+ /// - Parameters:
+ /// - addressComponents: The components of the selected address.
+ func didSelectAddress(_ addressComponents: [String: String])
+
+ /// Triggered when a tap occurs on a detected date.
+ ///
+ /// - Parameters:
+ /// - date: The selected date.
+ func didSelectDate(_ date: Date)
+
+ /// Triggered when a tap occurs on a detected phone number.
+ ///
+ /// - Parameters:
+ /// - phoneNumber: The selected phone number.
+ func didSelectPhoneNumber(_ phoneNumber: String)
+
+ /// Triggered when a tap occurs on a detected URL.
+ ///
+ /// - Parameters:
+ /// - url: The selected URL.
+ func didSelectURL(_ url: URL)
+
+ /// Triggered when a tap occurs on detected transit information.
+ ///
+ /// - Parameters:
+ /// - transitInformation: The selected transit information.
+ func didSelectTransitInformation(_ transitInformation: [String: String])
+
+ /// Triggered when a tap occurs on a mention
+ ///
+ /// - Parameters:
+ /// - mention: The selected mention
+ func didSelectMention(_ mention: String)
+
+ /// Triggered when a tap occurs on a hashtag
+ ///
+ /// - Parameters:
+ /// - mention: The selected hashtag
+ func didSelectHashtag(_ hashtag: String)
+
+ /// Triggered when a tap occurs on a custom regular expression
+ ///
+ /// - Parameters:
+ /// - pattern: the pattern of the regular expression
+ /// - match: part that match with the regular expression
+ func didSelectCustom(_ pattern: String, match: String?)
}
-public extension MessageLabelDelegate {
-
- func didSelectAddress(_ addressComponents: [String: String]) {}
+extension MessageLabelDelegate {
+ public func didSelectAddress(_: [String: String]) { }
- func didSelectDate(_ date: Date) {}
+ public func didSelectDate(_: Date) { }
- func didSelectPhoneNumber(_ phoneNumber: String) {}
+ public func didSelectPhoneNumber(_: String) { }
- func didSelectURL(_ url: URL) {}
-
- func didSelectTransitInformation(_ transitInformation: [String: String]) {}
+ public func didSelectURL(_: URL) { }
- func didSelectMention(_ mention: String) {}
+ public func didSelectTransitInformation(_: [String: String]) { }
- func didSelectHashtag(_ hashtag: String) {}
+ public func didSelectMention(_: String) { }
- func didSelectCustom(_ pattern: String, match: String?) {}
+ public func didSelectHashtag(_: String) { }
+ public func didSelectCustom(_: String, match _: String?) { }
}
diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift
index 664be32a4..d7c91786a 100644
--- a/Sources/Protocols/MessageType.swift
+++ b/Sources/Protocols/MessageType.swift
@@ -1,43 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// A standard protocol representing a message.
/// Use this protocol to create your own message object to be used by MessageKit.
public protocol MessageType {
+ /// The sender of the message.
+ var sender: SenderType { get }
- /// The sender of the message.
- var sender: SenderType { get }
+ /// The unique identifier for the message.
+ var messageId: String { get }
- /// The unique identifier for the message.
- var messageId: String { get }
-
- /// The date the message was sent.
- var sentDate: Date { get }
-
- /// The kind of message and its underlying kind.
- var kind: MessageKind { get }
+ /// The date the message was sent.
+ var sentDate: Date { get }
+ /// The kind of message and its underlying kind.
+ var kind: MessageKind { get }
}
diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift
index efb0a1357..831d1cf2f 100644
--- a/Sources/Protocols/MessagesDataSource.swift
+++ b/Sources/Protocols/MessagesDataSource.swift
@@ -1,183 +1,265 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
+// MARK: - MessagesDataSource
+
/// An object that adopts the `MessagesDataSource` protocol is responsible for providing
/// the data required by a `MessagesCollectionView`.
+@MainActor
public protocol MessagesDataSource: AnyObject {
+ /// The `SenderType` of new messages in the `MessagesCollectionView`.
+ var currentSender: SenderType { get }
+
+ /// A helper method to determine if a given message is from the current `SenderType`.
+ ///
+ /// - Parameters:
+ /// - message: The message to check if it was sent by the current `SenderType`.
+ ///
+ /// - Note:
+ /// The default implementation of this method checks for equality between
+ /// the message's `SenderType` and the current `SenderType`.
+ func isFromCurrentSender(message: MessageType) -> Bool
+
+ /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed.
+ func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType
+
+ /// The number of sections to be displayed in the `MessagesCollectionView`.
+ ///
+ /// - Parameters:
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
+ func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int
+
+ /// The number of cells to be displayed in the `MessagesCollectionView`.
+ ///
+ /// - Parameters:
+ /// - section: The number of the section in which the cells will be displayed.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
+ /// - Note:
+ /// The default implementation of this method returns 1. Putting each message in its own section.
+ func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int
+
+ /// The attributed text to be used for cell's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for message bubble's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// The attributed text to be used for cell's timestamp label.
+ /// The timestamp label is shown when showMessageTimestampOnSwipeLeft is enabled by swiping left over the chat controller.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// The default value returned by this method is `nil`.
+ func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
+
+ /// Text collectionView cell for message with `text`, `attributedText`, `emoji` message types.
+ ///
+ /// - Parameters:
+ /// - message: The `text`, `attributedText`, `emoji` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func textCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Photo or Video collectionView cell for message with `photo`, `video` message types.
+ ///
+ /// - Parameters:
+ /// - message: The `photo`, `video` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func photoCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Location collectionView cell for message with `location` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `location` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func locationCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Audio collectionView cell for message with `audio` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `audio` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func audioCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
+
+ /// Contact collectionView cell for message with `contact` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `contact` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will return nil by default. You must override this method only if you want your own cell.
+ func contactCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell?
- /// The `SenderType` of new messages in the `MessagesCollectionView`.
- func currentSender() -> SenderType
-
- /// A helper method to determine if a given message is from the current `SenderType`.
- ///
- /// - Parameters:
- /// - message: The message to check if it was sent by the current `SenderType`.
- ///
- /// - Note:
- /// The default implementation of this method checks for equality between
- /// the message's `SenderType` and the current `SenderType`.
- func isFromCurrentSender(message: MessageType) -> Bool
-
- /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed.
- func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType
-
- /// The number of sections to be displayed in the `MessagesCollectionView`.
- ///
- /// - Parameters:
- /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
- func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int
-
- /// The number of cells to be displayed in the `MessagesCollectionView`.
- ///
- /// - Parameters:
- /// - section: The number of the section in which the cells will be displayed.
- /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
- /// - Note:
- /// The default implementation of this method returns 1. Putting each message in its own section.
- func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int
-
- /// The attributed text to be used for cell's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for cell's bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for message bubble's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for cell's bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// The attributed text to be used for cell's timestamp label.
- /// The timestamp label is shown when showMessageTimestampOnSwipeLeft is enabled by swiping left over the chat controller.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// The default value returned by this method is `nil`.
- func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString?
-
- /// Custom collectionView cell for message with `custom` message type.
- ///
- /// - Parameters:
- /// - message: The `custom` message type
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages.
- func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
-
- /// Typing indicator cell used when the indicator is set to be shown
- ///
- /// - Parameters:
- /// - indexPath: The index path to dequeue the cell at
- /// - messagesCollectionView: The `MessagesCollectionView` the cell is to be rendered in
- /// - Returns: A `UICollectionViewCell` that indicates a user is typing
- func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
+ /// Custom collectionView cell for message with `custom` message type.
+ ///
+ /// - Parameters:
+ /// - message: The `custom` message type
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages.
+ func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell
+
+ /// Typing indicator cell used when the indicator is set to be shown
+ ///
+ /// - Parameters:
+ /// - indexPath: The index path to dequeue the cell at
+ /// - messagesCollectionView: The `MessagesCollectionView` the cell is to be rendered in
+ /// - Returns: A `UICollectionViewCell` that indicates a user is typing
+ func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell
}
-public extension MessagesDataSource {
-
- func isFromCurrentSender(message: MessageType) -> Bool {
- return message.sender.senderId == currentSender().senderId
- }
-
- func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int {
- return 1
- }
-
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- return nil
- }
-
- func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- let sentDate = message.sentDate
- let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate)
- let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10)
- let timeLabelColor: UIColor
- if #available(iOS 13, *) {
- timeLabelColor = .systemGray
- } else {
- timeLabelColor = .darkGray
- }
- return NSAttributedString(string: sentDateString, attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor])
- }
-
- func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell {
- fatalError(MessageKitError.customDataUnresolvedCell)
- }
-
- func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell {
- return messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath)
- }
+extension MessagesDataSource {
+ public func isFromCurrentSender(message: MessageType) -> Bool {
+ message.sender.senderId == currentSender.senderId
+ }
+
+ public func numberOfItems(inSection _: Int, in _: MessagesCollectionView) -> Int {
+ 1
+ }
+
+ public func cellTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
+ nil
+ }
+
+ public func messageTimestampLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
+ let sentDate = message.sentDate
+ let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate)
+ let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10)
+ let timeLabelColor: UIColor
+ timeLabelColor = .systemGray
+ return NSAttributedString(
+ string: sentDateString,
+ attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor])
+ }
+
+ public func textCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func photoCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func locationCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func audioCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func contactCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? {
+ nil
+ }
+
+ public func customCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell {
+ fatalError(MessageKitError.customDataUnresolvedCell)
+ }
+
+ public func typingIndicator(
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UICollectionViewCell
+ {
+ messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath)
+ }
}
diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift
index 2f8d0bdfc..81a5d6722 100644
--- a/Sources/Protocols/MessagesDisplayDelegate.swift
+++ b/Sources/Protocols/MessagesDisplayDelegate.swift
@@ -1,331 +1,408 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-import UIKit
import MapKit
+import UIKit
+
+// MARK: - MessagesDisplayDelegate
/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`.
+@MainActor
public protocol MessagesDisplayDelegate: AnyObject {
-
- // MARK: - All Messages
-
- /// Specifies the `MessageStyle` to be used for a `MessageContainerView`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is `MessageStyle.bubble`.
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle
-
- /// Specifies the background color of the `MessageContainerView`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value is `UIColor.clear` for emoji messages.
- /// For all other `MessageKind` cases, the color depends on the `SenderType`.
- ///
- /// Current sender: Green
- ///
- /// All other senders: Gray
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
-
- /// The section header to use for a given `IndexPath`.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this header.
- /// - indexPath: The `IndexPath` of the header.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
- func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
-
- /// The section footer to use for a given `IndexPath`.
- ///
- /// - Parameters:
- /// - indexPath: The `IndexPath` of the footer.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
- func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
-
- /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class.
- ///
- /// - Parameters:
- /// - avatarView: The `AvatarView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default image configured by this method is `?`.
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
- /// Used to configure the `AccessoryView` in a `MessageContentCell` class.
- ///
- /// - Parameters:
- /// - accessoryView: The `AccessoryView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default image configured by this method is `?`.
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
- // MARK: - Text Messages
-
- /// Specifies the color of the text for a `TextMessageCell`.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is determined by the messages `SenderType`.
- ///
- /// Current sender: UIColor.white
- ///
- /// All other senders: UIColor.darkText
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
-
- /// Specifies the `DetectorType`s to check for the `MessageType`'s text against.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// This method returns an empty array by default.
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType]
-
- /// Specifies the attributes for a given `DetectorType`
- ///
- /// - Parameters:
- /// - detector: The `DetectorType` for the applied attributes.
- /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
- /// - indexPath: The `IndexPath` of the cell.
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any]
-
- // MARK: - Location Messages
-
- /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style.
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions
-
- /// Used to configure the annoation view of the map image on the given location message.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The `MKAnnotationView` to use as the annotation view.
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView?
-
- /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message.
- /// The animation block is called with the `UIImageView` to be animated.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.location`.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
- /// - Returns: The animation block to use to apply the location image.
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
-
- // MARK: - Media Messages
-
- /// Used to configure the `UIImageView` of a `MediaMessageCell`.
- ///
- /// - Parameters:
- /// - imageView: The `UIImageView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
-
- // MARK: - Audio Message
-
- /// Used to configure the audio cell UI:
- /// 1. play button selected state;
- /// 2. progresssView progress;
- /// 3. durationLabel text;
- ///
- /// - Parameters:
- /// - cell: The `AudioMessageCell` that needs to be configure.
- /// - message: The `MessageType` that configures the cell.
- ///
- /// - Note:
- /// This protocol method is called by MessageKit every time an audio cell needs to be configure
- func configureAudioCell(_ cell: AudioMessageCell, message: MessageType)
-
- /// Specifies the tint color of play button and progress bar for an `AudioMessageCell`.
- ///
- /// - Parameters:
- /// - message: A `MessageType` with a `MessageKind` case of `.audio` to which the color will apply.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is UIColor.sendButtonBlue
- func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor
-
- /// Used to format the audio sound duration in a readable string
- ///
- /// - Parameters:
- /// - duration: The audio sound duration is seconds.
- /// - audioCell: The `AudioMessageCell` that ask for formated duration.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value is computed like fallow:
- /// 1. return the time as 0:ss if duration is up to 59 seconds (e.g. 0:03 means 0 minutes and 3 seconds)
- /// 2. return the time as m:ss if duration is greater than 59 and lower than 3600 (e.g. 12:23 means 12 mintues and 23 seconds)
- /// 3. return the time as h:mm:ss for anything longer that 3600 seconds (e.g. 1:19:08 means 1 hour 19 minutes and 8 seconds)
- func audioProgressTextFormat(_ duration: Float, for audioCell: AudioMessageCell, in messageCollectionView: MessagesCollectionView) -> String
-
- /// Used to configure the `UIImageView` of a `LinkPreviewMessageCell`.
- /// - Parameters:
- /// - imageView: The `UIImageView` of the cell.
- /// - message: The `MessageType` that will be displayed by this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- func configureLinkPreviewImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ // MARK: - All Messages
+
+ /// Specifies the `MessageStyle` to be used for a `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is `MessageStyle.bubble`.
+ func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> MessageStyle
+
+ /// Specifies the background color of the `MessageContainerView`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value is `UIColor.clear` for emoji messages.
+ /// For all other `MessageKind` cases, the color depends on the `SenderType`.
+ ///
+ /// Current sender: Green
+ ///
+ /// All other senders: Gray
+ func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// The section header to use for a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this header.
+ /// - indexPath: The `IndexPath` of the header.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
+ func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
+
+ /// The section footer to use for a given `IndexPath`.
+ ///
+ /// - Parameters:
+ /// - indexPath: The `IndexPath` of the footer.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
+ func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView
+
+ /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class.
+ ///
+ /// - Parameters:
+ /// - avatarView: The `AvatarView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default image configured by this method is `?`.
+ func configureAvatarView(
+ _ avatarView: AvatarView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ /// Used to configure the `AccessoryView` in a `MessageContentCell` class.
+ ///
+ /// - Parameters:
+ /// - accessoryView: The `AccessoryView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default image configured by this method is `?`.
+ func configureAccessoryView(
+ _ accessoryView: UIView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ // MARK: - Text Messages
+
+ /// Specifies the color of the text for a `TextMessageCell`.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is determined by the messages `SenderType`.
+ ///
+ /// Current sender: UIColor.white
+ ///
+ /// All other senders: UIColor.darkText
+ func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// Specifies the `DetectorType`s to check for the `MessageType`'s text against.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// This method returns an empty array by default.
+ func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> [DetectorType]
+
+ /// Specifies the attributes for a given `DetectorType`
+ ///
+ /// - Parameters:
+ /// - detector: The `DetectorType` for the applied attributes.
+ /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath)
+ -> [NSAttributedString.Key: Any]
+
+ // MARK: - Location Messages
+
+ /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style.
+ func snapshotOptionsForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions
+
+ /// Used to configure the annotation view of the map image on the given location message.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The `MKAnnotationView` to use as the annotation view.
+ func annotationViewForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messageCollectionView: MessagesCollectionView) -> MKAnnotationView?
+
+ /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message.
+ /// The animation block is called with the `UIImageView` to be animated.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.location`.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` requesting the information.
+ /// - Returns: The animation block to use to apply the location image.
+ func animationBlockForLocation(
+ message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
+
+ // MARK: - Media Messages
+
+ /// Used to configure the `UIImageView` of a `MediaMessageCell`.
+ ///
+ /// - Parameters:
+ /// - imageView: The `UIImageView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ func configureMediaMessageImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+
+ // MARK: - Audio Message
+
+ /// Used to configure the audio cell UI:
+ /// 1. play button selected state;
+ /// 2. progressView progress;
+ /// 3. durationLabel text;
+ ///
+ /// - Parameters:
+ /// - cell: The `AudioMessageCell` that needs to be configure.
+ /// - message: The `MessageType` that configures the cell.
+ ///
+ /// - Note:
+ /// This protocol method is called by MessageKit every time an audio cell needs to be configure
+ func configureAudioCell(_ cell: AudioMessageCell, message: MessageType)
+
+ /// Specifies the tint color of play button and progress bar for an `AudioMessageCell`.
+ ///
+ /// - Parameters:
+ /// - message: A `MessageType` with a `MessageKind` case of `.audio` to which the color will apply.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is UIColor.sendButtonBlue
+ func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+
+ /// Used to format the audio sound duration in a readable string
+ ///
+ /// - Parameters:
+ /// - duration: The audio sound duration is seconds.
+ /// - audioCell: The `AudioMessageCell` that ask for formated duration.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value is computed like fallow:
+ /// 1. return the time as 0:ss if duration is up to 59 seconds (e.g. 0:03 means 0 minutes and 3 seconds)
+ /// 2. return the time as m:ss if duration is greater than 59 and lower than 3600 (e.g. 12:23 means 12 minutes and 23 seconds)
+ /// 3. return the time as h:mm:ss for anything longer that 3600 seconds (e.g. 1:19:08 means 1 hour 19 minutes and 8 seconds)
+ func audioProgressTextFormat(
+ _ duration: Float,
+ for audioCell: AudioMessageCell,
+ in messageCollectionView: MessagesCollectionView) -> String
+
+ /// Used to configure the `UIImageView` of a `LinkPreviewMessageCell`.
+ /// - Parameters:
+ /// - imageView: The `UIImageView` of the cell.
+ /// - message: The `MessageType` that will be displayed by this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ func configureLinkPreviewImageView(
+ _ imageView: UIImageView,
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
}
-public extension MessagesDisplayDelegate {
-
- // MARK: - All Messages Defaults
-
- func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
- return .bubble
- }
-
- func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
-
- switch message.kind {
- case .emoji:
- return .clear
- default:
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- return .white
- }
- return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageBackground : .incomingMessageBackground
- }
- }
-
- func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView {
- return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath)
- }
-
- func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView {
- return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath)
- }
-
- func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- avatarView.initials = "?"
- }
-
- func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {}
-
- // MARK: - Text Messages Defaults
-
- func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageLabel : .incomingMessageLabel
- }
-
- func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
- return []
+extension MessagesDisplayDelegate {
+ // MARK: - All Messages Defaults
+
+ public func messageStyle(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
+ .bubble
+ }
+
+ public func backgroundColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ switch message.kind {
+ case .emoji:
+ return .clear
+ default:
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ return .white
+ }
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageBackground : .incomingMessageBackground
}
-
- func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] {
- return MessageLabel.defaultAttributes
+ }
+
+ public func messageHeaderView(
+ for indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> MessageReusableView
+ {
+ messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath)
+ }
+
+ public func messageFooterView(
+ for indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> MessageReusableView
+ {
+ messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath)
+ }
+
+ public func configureAvatarView(_ avatarView: AvatarView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
+ avatarView.initials = "?"
+ }
+
+ public func configureAccessoryView(_: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { }
+
+ // MARK: - Text Messages Defaults
+
+ public func textColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- // MARK: - Location Messages Defaults
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
- return LocationMessageSnapshotOptions()
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageLabel : .incomingMessageLabel
+ }
+
+ public func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
+ []
+ }
+
+ public func detectorAttributes(for _: DetectorType, and _: MessageType, at _: IndexPath) -> [NSAttributedString.Key: Any] {
+ MessageLabel.defaultAttributes
+ }
+
+ // MARK: - Location Messages Defaults
+
+ public func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions()
+ }
+
+ public func annotationViewForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> MKAnnotationView?
+ {
+ MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
+ }
+
+ public func animationBlockForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
+ {
+ nil
+ }
+
+ // MARK: - Media Message Defaults
+
+ public func configureMediaMessageImageView(
+ _: UIImageView,
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) { }
+
+ // MARK: - Audio Message Defaults
+
+ public func configureAudioCell(_: AudioMessageCell, message _: MessageType) { }
+
+ public func audioTintColor(
+ for message: MessageType,
+ at _: IndexPath,
+ in messagesCollectionView: MessagesCollectionView)
+ -> UIColor
+ {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
- return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
+ return dataSource.isFromCurrentSender(message: message) ? .outgoingAudioMessageTint : .incomingAudioMessageTint
+ }
+
+ public func audioProgressTextFormat(_ duration: Float, for _: AudioMessageCell, in _: MessagesCollectionView) -> String {
+ var returnValue = "0:00"
+ // print the time as 0:ss if duration is up to 59 seconds
+ // print the time as m:ss if duration is up to 59:59 seconds
+ // print the time as h:mm:ss for anything longer
+ if duration < 60 {
+ 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 if duration.isFinite {
+ let hours = Int(duration / 3600)
+ let remainingMinutesInSeconds = Int(duration) - hours * 3600
+ returnValue = String(
+ format: "%.02d:%.02d:%.02d",
+ hours,
+ Int(remainingMinutesInSeconds / 60),
+ Int(remainingMinutesInSeconds) % 60)
}
+ return returnValue
+ }
- func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
- return nil
- }
-
- // MARK: - Media Message Defaults
-
- func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- }
+ // MARK: - LinkPreview Message Defaults
- // MARK: - Audio Message Defaults
-
- func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
-
- }
-
- func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- return dataSource.isFromCurrentSender(message: message) ? .outgoingAudioMessageTint : .incomingAudioMessageTint
- }
-
- func audioProgressTextFormat(_ duration: Float, for audioCell: AudioMessageCell, in messageCollectionView: MessagesCollectionView) -> String {
- var retunValue = "0:00"
- // print the time as 0:ss if duration is up to 59 seconds
- // print the time as m:ss if duration is up to 59:59 seconds
- // print the time as h:mm:ss for anything longer
- if duration < 60 {
- retunValue = String(format: "0:%.02d", Int(duration.rounded(.up)))
- } else if duration < 3600 {
- retunValue = String(format: "%.02d:%.02d", Int(duration/60), Int(duration) % 60)
- } else {
- let hours = Int(duration/3600)
- let remainingMinutsInSeconds = Int(duration) - hours*3600
- retunValue = String(format: "%.02d:%.02d:%.02d", hours, Int(remainingMinutsInSeconds/60), Int(remainingMinutsInSeconds) % 60)
- }
- return retunValue
- }
-
- // MARK: - LinkPreview Message Defaults
-
- func configureLinkPreviewImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
- }
+ public func configureLinkPreviewImageView(
+ _: UIImageView,
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView) { }
}
diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift
index a3d9ff5a8..015c45c30 100644
--- a/Sources/Protocols/MessagesLayoutDelegate.swift
+++ b/Sources/Protocols/MessagesLayoutDelegate.swift
@@ -1,166 +1,413 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
+// MARK: - MessagesLayoutDelegate
+
/// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine
/// the size and layout of a `MessageCollectionViewCell` and its contents.
+@MainActor
public protocol MessagesLayoutDelegate: AnyObject {
+ /// Specifies the size to use for a header view.
+ ///
+ /// - Parameters:
+ /// - section: The section number of the header.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a size of `GGSize.zero`.
+ func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
+
+ /// Specifies the size to use for a footer view.
+ ///
+ /// - Parameters:
+ /// - section: The section number of the footer.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a size of `GGSize.zero`.
+ func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
+
+ /// Specifies the size to use for a typing indicator view.
+ ///
+ /// - Parameters:
+ /// - layout: The `MessagesCollectionViewFlowLayout` layout.
+ ///
+ /// - Note:
+ /// The default value returned by this method is the width of the `messagesCollectionView` minus insets and a height of 62.
+ func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize
+
+ /// Specifies the top inset to use for a typing indicator view.
+ ///
+ /// - Parameters:
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is a top inset of 15.
+ func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the height for the `MessageContentCell`'s top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> CGFloat
+
+ /// Specifies the height for the `MessageContentCell`'s bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func cellBottomLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the height for the message bubble's top label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func messageTopLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the label alignment for the message bubble's top label.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional LabelAlignment for the message bubble's top label. If nil is returned or the delegate method is not implemented,
+ /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming
+ func messageTopLabelAlignment(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
+
+ /// Specifies the height for the `MessageContentCell`'s bottom label.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default value returned by this method is zero.
+ func messageBottomLabelHeight(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CGFloat
+
+ /// Specifies the label alignment for the message bubble's bottom label.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional LabelAlignment for the message bubble's bottom label. If nil is returned or the delegate method is not implemented,
+ /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming
+ func messageBottomLabelAlignment(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> LabelAlignment?
+
+ /// Specifies the size for the `MessageContentCell`'s avatar image view.
+ /// - Parameters:
+ /// - message: The `MessageType` that will be displayed for this cell.
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ /// - Returns: Optional CGSize for the avatar image view. If nil is returned or delegate method is not implemented,
+ /// size from `MessageSizeCalculator`'s `incomingAvatarSize` or `outgoingAvatarSize` will be used depending if the message is outgoing or incoming
+ func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
+ -> CGSize?
+
+ /// Text cell size calculator for messages with MessageType.text.
+ ///
+ /// - Parameters:
+ /// - message: The text message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text.
+ func textCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Attributed text cell size calculator for messages with MessageType.attributedText.
+ ///
+ /// - Parameters:
+ /// - message: The attributedText message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.attributedText.
+ func attributedTextCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Emoji cell size calculator for messages with MessageType.emoji.
+ ///
+ /// - Parameters:
+ /// - message: The emoji message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.emoji.
+ func emojiCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Photo cell size calculator for messages with MessageType.photo.
+ ///
+ /// - Parameters:
+ /// - message: The photo message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text.
+ func photoCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Video cell size calculator for messages with MessageType.video.
+ ///
+ /// - Parameters:
+ /// - message: The video message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.video.
+ func videoCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Location cell size calculator for messages with MessageType.location.
+ ///
+ /// - Parameters:
+ /// - message: The location message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.location.
+ func locationCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Audio cell size calculator for messages with MessageType.audio.
+ ///
+ /// - Parameters:
+ /// - message: The audio message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.audio.
+ func audioCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
+
+ /// Contact cell size calculator for messages with MessageType.contact.
+ ///
+ /// - Parameters:
+ /// - message: The contact message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.contact.
+ func contactCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator?
- /// Specifies the size to use for a header view.
- ///
- /// - Parameters:
- /// - section: The section number of the header.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is a size of `GGSize.zero`.
- func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
-
- /// Specifies the size to use for a footer view.
- ///
- /// - Parameters:
- /// - section: The section number of the footer.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is a size of `GGSize.zero`.
- func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize
-
- /// Specifies the size to use for a typing indicator view.
- ///
- /// - Parameters:
- /// - layout: The `MessagesCollectionViewFlowLayout` layout.
- ///
- /// - Note:
- /// The default value returned by this method is the width of the `messagesCollectionView` minus insets and a height of 62.
- func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize
-
- /// Specifies the top inset to use for a typing indicator view.
- ///
- /// - Parameters:
- /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is a top inset of 15.
- func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the `MessageContentCell`'s top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the `MessageContentCell`'s bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the message bubble's top label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Specifies the height for the `MessageContentCell`'s bottom label.
- ///
- /// - Parameters:
- /// - message: The `MessageType` that will be displayed for this cell.
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default value returned by this method is zero.
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat
-
- /// Custom cell size calculator for messages with MessageType.custom.
- ///
- /// - Parameters:
- /// - message: The custom message
- /// - indexPath: The `IndexPath` of the cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
- ///
- /// - Note:
- /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom.
- func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
+ /// Custom cell size calculator for messages with MessageType.custom.
+ ///
+ /// - Parameters:
+ /// - message: The custom message
+ /// - indexPath: The `IndexPath` of the cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
+ ///
+ /// - Note:
+ /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom.
+ func customCellSizeCalculator(
+ for message: MessageType,
+ at indexPath: IndexPath,
+ in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator
}
-public extension MessagesLayoutDelegate {
-
- func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- return .zero
- }
-
- func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- return .zero
- }
-
- func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize {
- let collectionViewWidth = layout.messagesCollectionView.bounds.width
- let contentInset = layout.messagesCollectionView.contentInset
- let inset = layout.sectionInset.horizontal + contentInset.horizontal
- return CGSize(width: collectionViewWidth - inset, height: 62)
- }
-
- func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 15
- }
-
- func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0
- }
-
- func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator {
- fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)")
- }
+extension MessagesLayoutDelegate {
+ public func headerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize {
+ .zero
+ }
+
+ public func footerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize {
+ .zero
+ }
+
+ public func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize {
+ let collectionViewWidth = layout.messagesCollectionView.bounds.width
+ let contentInset = layout.messagesCollectionView.contentInset
+ let inset = layout.sectionInset.horizontal + contentInset.horizontal
+ return CGSize(width: collectionViewWidth - inset, height: 62)
+ }
+
+ public func typingIndicatorViewTopInset(in _: MessagesCollectionView) -> CGFloat {
+ 15
+ }
+
+ public func cellTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func cellBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func messageTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment? {
+ nil
+ }
+
+ public func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat {
+ 0
+ }
+
+ public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize? {
+ nil
+ }
+
+ public func messageBottomLabelAlignment(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LabelAlignment?
+ {
+ nil
+ }
+
+ public func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator? {
+ nil
+ }
+
+ public func attributedTextCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func emojiCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func photoCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func videoCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func locationCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func audioCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func contactCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator?
+ {
+ nil
+ }
+
+ public func customCellSizeCalculator(
+ for _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> CellSizeCalculator
+ {
+ fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)")
+ }
}
diff --git a/Sources/Protocols/SenderType.swift b/Sources/Protocols/SenderType.swift
index 4c5c19a7b..e22605f0e 100644
--- a/Sources/Protocols/SenderType.swift
+++ b/Sources/Protocols/SenderType.swift
@@ -1,38 +1,35 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
/// A standard protocol representing a sender.
/// Use this protocol to adhere a object as the sender of a MessageType
public protocol SenderType {
+ /// The unique String identifier for the sender.
+ ///
+ /// Note: This value must be unique across all senders.
+ var senderId: String { get }
- /// The unique String identifier for the sender.
- ///
- /// Note: This value must be unique across all senders.
- var senderId: String { get }
-
- /// The display name of a sender.
- var displayName: String { get }
+ /// The display name of a sender.
+ var displayName: String { get }
}
diff --git a/Sources/Supporting/MessageInputBar.swift b/Sources/Supporting/MessageInputBar.swift
deleted file mode 100644
index 7c6f7dbf1..000000000
--- a/Sources/Supporting/MessageInputBar.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import UIKit
-import InputBarAccessoryView
-
-@available(*, unavailable, renamed: "InputBarAccessoryView")
-public typealias MessageInputBar = InputBarAccessoryView
-
-@available(*, unavailable, renamed: "InputBarAccessoryViewDelegate")
-public typealias MessageInputBarDelegate = InputBarAccessoryViewDelegate
diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift
deleted file mode 100644
index 03d3083fe..000000000
--- a/Sources/Supporting/MessageKit+Availability.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
-
-import Foundation
-import UIKit
-
-public extension MessagesLayoutDelegate {
-
- func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
- fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition {
- fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
- fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0")
- }
-
- func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
- fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
- fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
- fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.")
- }
-
- func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.")
- }
-
- func shouldCacheLayoutAttributes(for message: MessageType) -> Bool {
- fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.")
- }
-}
diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift
index 6e5bd2068..3fe8ed56e 100644
--- a/Sources/Views/AvatarView.swift
+++ b/Sources/Views/AvatarView.swift
@@ -1,198 +1,218 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
+// MARK: - AvatarView
+
open class AvatarView: UIImageView {
+ // MARK: Lifecycle
- // MARK: - Properties
-
- open var initials: String? {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ // MARK: - Initializers
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ prepareView()
+ }
- open var placeholderFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ prepareView()
+ }
- open var placeholderTextColor: UIColor = .white {
- didSet {
- setImageFrom(initials: initials)
- }
- }
+ convenience public init() {
+ self.init(frame: .zero)
+ prepareView()
+ }
- open var fontMinimumScaleFactor: CGFloat = 0.5
+ // MARK: Open
- open var adjustsFontSizeToFitWidth = true
+ open var fontMinimumScaleFactor: CGFloat = 0.5
- private var minimumFontSize: CGFloat {
- return placeholderFont.pointSize * fontMinimumScaleFactor
- }
+ open var adjustsFontSizeToFitWidth = true
- private var radius: CGFloat?
+ // MARK: - Properties
- // MARK: - Overridden Properties
- open override var frame: CGRect {
- didSet {
- setCorner(radius: self.radius)
- }
+ open var initials: String? {
+ didSet {
+ setImageFrom(initials: initials)
}
+ }
- open override var bounds: CGRect {
- didSet {
- setCorner(radius: self.radius)
- }
- }
-
- // MARK: - Initializers
- public override init(frame: CGRect) {
- super.init(frame: frame)
- prepareView()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- prepareView()
- }
-
- convenience public init() {
- self.init(frame: .zero)
- prepareView()
- }
-
- private func setImageFrom(initials: String?) {
- guard let initials = initials else { return }
- image = getImageFrom(initials: initials)
+ open var placeholderFont = UIFont.preferredFont(forTextStyle: .caption1) {
+ didSet {
+ setImageFrom(initials: initials)
}
+ }
- private func getImageFrom(initials: String) -> UIImage {
- let width = frame.width
- let height = frame.height
- if width == 0 || height == 0 {return UIImage()}
- var font = placeholderFont
-
- UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale)
- defer { UIGraphicsEndImageContext() }
- let context = UIGraphicsGetCurrentContext()!
-
- //// Text Drawing
- let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height)
- let initialsText = NSAttributedString(string: initials, attributes: [.font: font])
- if adjustsFontSizeToFitWidth,
- initialsText.width(considering: textRect.height) > textRect.width {
- let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height)
- font = placeholderFont.withSize(newFontSize)
- }
-
- let textStyle = NSMutableParagraphStyle()
- textStyle.alignment = .center
- let textFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: placeholderTextColor, NSAttributedString.Key.paragraphStyle: textStyle]
-
- let textTextHeight: CGFloat = initials.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
- context.saveGState()
- context.clip(to: textRect)
- initials.draw(in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), withAttributes: textFontAttributes)
- context.restoreGState()
- guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() else { assertionFailure("Could not create image from context"); return UIImage()}
- return renderedImage
+ open var placeholderTextColor: UIColor = .white {
+ didSet {
+ setImageFrom(initials: initials)
}
-
- /**
- Recursively find the biggest size to fit the text with a given width and height
- */
- private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat {
- let attributedText = NSAttributedString(string: text, attributes: [.font: font])
- if attributedText.width(considering: height) > width {
- let newFont = font.withSize(font.pointSize - 1)
- if newFont.pointSize > minimumFontSize {
- return font.pointSize
- } else {
- return calculateFontSize(text: text, font: newFont, width: width, height: height)
- }
- }
+ }
+
+ // MARK: - Overridden Properties
+ open override var frame: CGRect {
+ didSet {
+ setCorner(radius: self.radius)
+ }
+ }
+
+ open override var bounds: CGRect {
+ didSet {
+ setCorner(radius: self.radius)
+ }
+ }
+
+ // MARK: - Open setters
+
+ open func set(avatar: Avatar) {
+ if let image = avatar.image {
+ self.image = image
+ } else {
+ initials = avatar.initials
+ }
+ }
+
+ open func setCorner(radius: CGFloat?) {
+ guard let radius = radius else {
+ // if corner radius not set default to Circle
+ let cornerRadius = min(frame.width, frame.height)
+ layer.cornerRadius = cornerRadius / 2
+ return
+ }
+ self.radius = radius
+ layer.cornerRadius = radius
+ }
+
+ // MARK: Internal
+
+ // MARK: - Internal methods
+
+ internal func prepareView() {
+ backgroundColor = .avatarViewBackground
+ contentMode = .scaleAspectFill
+ layer.masksToBounds = true
+ clipsToBounds = true
+ setCorner(radius: nil)
+ }
+
+ // MARK: Private
+
+ private var radius: CGFloat?
+
+ private var minimumFontSize: CGFloat {
+ placeholderFont.pointSize * fontMinimumScaleFactor
+ }
+
+ private func setImageFrom(initials: String?) {
+ guard let initials = initials else { return }
+ image = getImageFrom(initials: initials)
+ }
+
+ private func getImageFrom(initials: String) -> UIImage {
+ let width = frame.width
+ let height = frame.height
+ if width == 0 || height == 0 { return UIImage() }
+ var font = placeholderFont
+
+ UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale)
+ defer { UIGraphicsEndImageContext() }
+ let context = UIGraphicsGetCurrentContext()!
+
+ //// Text Drawing
+ let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height)
+ let initialsText = NSAttributedString(string: initials, attributes: [.font: font])
+ if
+ adjustsFontSizeToFitWidth,
+ initialsText.width(considering: textRect.height) > textRect.width
+ {
+ let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height)
+ font = placeholderFont.withSize(newFontSize)
+ }
+
+ let textStyle = NSMutableParagraphStyle()
+ textStyle.alignment = .center
+ let textFontAttributes: [NSAttributedString.Key: Any] = [
+ NSAttributedString.Key.font: font,
+ NSAttributedString.Key.foregroundColor: placeholderTextColor,
+ NSAttributedString.Key.paragraphStyle: textStyle,
+ ]
+
+ let textTextHeight: CGFloat = initials.boundingRect(
+ with: CGSize(width: textRect.width, height: CGFloat.infinity),
+ options: .usesLineFragmentOrigin,
+ attributes: textFontAttributes,
+ context: nil).height
+ context.saveGState()
+ context.clip(to: textRect)
+ initials.draw(
+ in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight),
+ withAttributes: textFontAttributes)
+ context.restoreGState()
+ guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext()
+ else { assertionFailure("Could not create image from context"); return UIImage() }
+ return renderedImage
+ }
+
+ /// Recursively find the biggest size to fit the text with a given width and height
+ private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat {
+ let attributedText = NSAttributedString(string: text, attributes: [.font: font])
+ if attributedText.width(considering: height) > width {
+ let newFont = font.withSize(font.pointSize - 1)
+ if newFont.pointSize > minimumFontSize {
return font.pointSize
- }
-
- /**
- Calculates the inner circle's width.
- Note: Assumes corner radius cannot be more than width/2 (this creates circle).
- */
- private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect {
- guard outerViewWidth > 0 else {
- return CGRect.zero
- }
- let shortEdge = min(outerViewHeight, outerViewWidth)
- // Converts degree to radian degree and calculate the
- // Assumes, it is a perfect circle based on the shorter part of ellipsoid
- // calculate a rectangle
- let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2
- let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2
- let startX = (outerViewWidth - w)/2
- let startY = (outerViewHeight - h)/2
- // In case the font exactly fits to the region, put 2 pixel both left and right
- return CGRect(startX+2, startY, w-4, h)
- }
-
- // MARK: - Internal methods
-
- internal func prepareView() {
- backgroundColor = .avatarViewBackground
- contentMode = .scaleAspectFill
- layer.masksToBounds = true
- clipsToBounds = true
- setCorner(radius: nil)
- }
+ } else {
+ return calculateFontSize(text: text, font: newFont, width: width, height: height)
+ }
+ }
+ return font.pointSize
+ }
+
+ /// Calculates the inner circle's width.
+ /// - Note: Assumes corner radius cannot be more than width/2 (this creates circle).
+ private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect {
+ guard outerViewWidth > 0 else {
+ return CGRect.zero
+ }
+ let shortEdge = min(outerViewHeight, outerViewWidth)
+ // Converts degree to radian degree and calculate the
+ // Assumes, it is a perfect circle based on the shorter part of ellipsoid
+ // calculate a rectangle
+ let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2
+ let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2
+ let startX = (outerViewWidth - w) / 2
+ let startY = (outerViewHeight - h) / 2
+ // In case the font exactly fits to the region, put 2 pixel both left and right
+ return CGRect(startX + 2, startY, w - 4, h)
+ }
+}
- // MARK: - Open setters
-
- open func set(avatar: Avatar) {
- if let image = avatar.image {
- self.image = image
- } else {
- initials = avatar.initials
- }
- }
+extension FloatingPoint {
+ // MARK: Fileprivate
- open func setCorner(radius: CGFloat?) {
- guard let radius = radius else {
- //if corner radius not set default to Circle
- let cornerRadius = min(frame.width, frame.height)
- layer.cornerRadius = cornerRadius/2
- return
- }
- self.radius = radius
- layer.cornerRadius = radius
- }
+ fileprivate var degreesToRadians: Self { self * .pi / 180 }
-}
+ // MARK: Private
-fileprivate extension FloatingPoint {
- var degreesToRadians: Self { return self * .pi / 180 }
- var radiansToDegrees: Self { return self * 180 / .pi }
+ private var radiansToDegrees: Self { self * 180 / .pi }
}
diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift
index c0c276ad8..93acf6f0b 100644
--- a/Sources/Views/BubbleCircle.swift
+++ b/Sources/Views/BubbleCircle.swift
@@ -1,49 +1,48 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A `UIView` subclass that maintains a mask to keep it fully circular
open class BubbleCircle: UIView {
-
- /// Lays out subviews and applys a circular mask to the layer
- open override func layoutSubviews() {
- super.layoutSubviews()
- layer.mask = roundedMask(corners: .allCorners, radius: bounds.height / 2)
- }
-
- /// Returns a rounded mask of the view
- ///
- /// - Parameters:
- /// - corners: The corners to round
- /// - radius: The radius of curve
- /// - Returns: A mask
- open func roundedMask(corners: UIRectCorner, radius: CGFloat) -> CAShapeLayer {
- let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
- let mask = CAShapeLayer()
- mask.path = path.cgPath
- return mask
- }
-
+ /// Lays out subviews and applies a circular mask to the layer
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ layer.mask = roundedMask(corners: .allCorners, radius: bounds.height / 2)
+ }
+
+ /// Returns a rounded mask of the view
+ ///
+ /// - Parameters:
+ /// - corners: The corners to round
+ /// - radius: The radius of curve
+ /// - Returns: A mask
+ open func roundedMask(corners: UIRectCorner, radius: CGFloat) -> CAShapeLayer {
+ let path = UIBezierPath(
+ roundedRect: bounds,
+ byRoundingCorners: corners,
+ cornerRadii: CGSize(width: radius, height: radius))
+ let mask = CAShapeLayer()
+ mask.path = path.cgPath
+ return mask
+ }
}
diff --git a/Sources/Views/Cells/AudioMessageCell.swift b/Sources/Views/Cells/AudioMessageCell.swift
index 59f473c30..e32ecd8cd 100644
--- a/Sources/Views/Cells/AudioMessageCell.swift
+++ b/Sources/Views/Cells/AudioMessageCell.swift
@@ -1,141 +1,162 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import AVFoundation
+import UIKit
/// A subclass of `MessageContentCell` used to display video and audio messages.
open class AudioMessageCell: MessageContentCell {
-
- /// The play button view to display on audio messages.
- public lazy var playButton: UIButton = {
- let playButton = UIButton(type: .custom)
- let playImage = UIImage.messageKitImageWith(type: .play)
- let pauseImage = UIImage.messageKitImageWith(type: .pause)
- playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal)
- playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected)
- return playButton
- }()
-
- /// The time duration lable to display on audio messages.
- public lazy var durationLabel: UILabel = {
- let durationLabel = UILabel(frame: CGRect.zero)
- durationLabel.textAlignment = .right
- durationLabel.font = UIFont.systemFont(ofSize: 14)
- durationLabel.text = "0:00"
- 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)
+ // MARK: Open
+
+ /// 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)
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(playButton)
+ messageContainerView.addSubview(activityIndicatorView)
+ messageContainerView.addSubview(durationLabel)
+ messageContainerView.addSubview(progressView)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ progressView.progress = 0
+ playButton.isSelected = false
+ activityIndicatorView.stopAnimating()
+ playButton.isHidden = false
+ durationLabel.text = "0:00"
+ }
+
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: self)
+ // compute play button touch area, currently play button size is (25, 25) which is hardly touchable
+ // add 10 px around current button frame and test the touch against this new frame
+ let playButtonTouchArea = CGRect(
+ playButton.frame.origin.x - 10.0,
+ playButton.frame.origin.y - 10,
+ playButton.frame.size.width + 20,
+ playButton.frame.size.height + 20)
+ let translateTouchLocation = convert(touchLocation, to: messageContainerView)
+ if playButtonTouchArea.contains(translateTouchLocation) {
+ delegate?.didTapPlayButton(in: self)
+ } else {
+ super.handleTapGesture(gesture)
}
+ }
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(playButton)
- messageContainerView.addSubview(activityIndicatorView)
- messageContainerView.addSubview(durationLabel)
- messageContainerView.addSubview(progressView)
- setupConstraints()
- }
+ // MARK: - Configure Cell
- open override func prepareForReuse() {
- super.prepareForReuse()
- progressView.progress = 0
- playButton.isSelected = false
- activityIndicatorView.stopAnimating()
- playButton.isHidden = false
- durationLabel.text = "0:00"
- }
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
- /// Handle tap gesture on contentView and its subviews.
- open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
- let touchLocation = gesture.location(in: self)
- // compute play button touch area, currently play button size is (25, 25) which is hardly touchable
- // add 10 px around current button frame and test the touch against this new frame
- let playButtonTouchArea = CGRect(playButton.frame.origin.x - 10.0, playButton.frame.origin.y - 10, playButton.frame.size.width + 20, playButton.frame.size.height + 20)
- let translateTouchLocation = convert(touchLocation, to: messageContainerView)
- if playButtonTouchArea.contains(translateTouchLocation) {
- delegate?.didTapPlayButton(in: self)
- } else {
- super.handleTapGesture(gesture)
- }
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
- // MARK: - Configure Cell
-
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
+ let playButtonLeftConstraint = messageContainerView.constraints.filter { $0.identifier == "left" }.first
+ let durationLabelRightConstraint = messageContainerView.constraints.filter { $0.identifier == "right" }.first
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
-
- let playButtonLeftConstraint = messageContainerView.constraints.filter { $0.identifier == "left" }.first
- let durationLabelRightConstraint = messageContainerView.constraints.filter { $0.identifier == "right" }.first
-
- if !dataSource.isFromCurrentSender(message: message) {
- playButtonLeftConstraint?.constant = 12
- durationLabelRightConstraint?.constant = -8
- } else {
- playButtonLeftConstraint?.constant = 5
- durationLabelRightConstraint?.constant = -15
- }
+ if !dataSource.isFromCurrentSender(message: message) {
+ playButtonLeftConstraint?.constant = 12
+ durationLabelRightConstraint?.constant = -8
+ } else {
+ playButtonLeftConstraint?.constant = 5
+ durationLabelRightConstraint?.constant = -15
+ }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ }
- let tintColor = displayDelegate.audioTintColor(for: message, at: indexPath, in: messagesCollectionView)
- playButton.imageView?.tintColor = tintColor
- durationLabel.textColor = tintColor
- progressView.tintColor = tintColor
-
- if case let .audio(audioItem) = message.kind {
- durationLabel.text = displayDelegate.audioProgressTextFormat(audioItem.duration, for: self, in: messagesCollectionView)
- }
+ let tintColor = displayDelegate.audioTintColor(for: message, at: indexPath, in: messagesCollectionView)
+ playButton.imageView?.tintColor = tintColor
+ durationLabel.textColor = tintColor
+ progressView.tintColor = tintColor
- displayDelegate.configureAudioCell(self, message: message)
+ if case .audio(let audioItem) = message.kind {
+ durationLabel.text = displayDelegate.audioProgressTextFormat(
+ audioItem.duration,
+ for: self,
+ in: messagesCollectionView)
}
+
+ displayDelegate.configureAudioCell(self, message: message)
+ }
+
+ // MARK: Public
+
+ /// The play button view to display on audio messages.
+ public lazy var playButton: UIButton = {
+ let playButton = UIButton(type: .custom)
+ let playImage = UIImage.messageKitImageWith(type: .play)
+ let pauseImage = UIImage.messageKitImageWith(type: .pause)
+ playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal)
+ playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected)
+ return playButton
+ }()
+
+ /// The time duration label to display on audio messages.
+ public lazy var durationLabel: UILabel = {
+ let durationLabel = UILabel(frame: CGRect.zero)
+ durationLabel.textAlignment = .right
+ durationLabel.font = UIFont.systemFont(ofSize: 14)
+ durationLabel.text = "0:00"
+ return durationLabel
+ }()
+
+ public lazy var activityIndicatorView: UIActivityIndicatorView = {
+ let activityIndicatorView = UIActivityIndicatorView(style: .medium)
+ activityIndicatorView.hidesWhenStopped = true
+ activityIndicatorView.isHidden = true
+ return activityIndicatorView
+ }()
+
+ public lazy var progressView: UIProgressView = {
+ let progressView = UIProgressView(progressViewStyle: .default)
+ progressView.progress = 0.0
+ return progressView
+ }()
}
diff --git a/Sources/Views/Cells/ContactMessageCell.swift b/Sources/Views/Cells/ContactMessageCell.swift
index 0ffbb8502..cc3e64cc0 100644
--- a/Sources/Views/Cells/ContactMessageCell.swift
+++ b/Sources/Views/Cells/ContactMessageCell.swift
@@ -1,143 +1,152 @@
-/*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2018 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class ContactMessageCell: MessageContentCell {
-
- public enum ConstraintsID: String {
- case initialsContainerLeftConstraint
- case disclosureRightConstraint
- }
-
- /// The view container that holds contact initials
- public lazy var initialsContainerView: UIView = {
- let initialsContainer = UIView(frame: CGRect.zero)
- initialsContainer.backgroundColor = .collectionViewBackground
- return initialsContainer
- }()
-
- /// The label that display the contact initials
- public lazy var initialsLabel: UILabel = {
- let initialsLabel = UILabel(frame: CGRect.zero)
- initialsLabel.textAlignment = .center
- initialsLabel.textColor = .label
- initialsLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
- return initialsLabel
- }()
-
- /// The label that display contact name
- public lazy var nameLabel: UILabel = {
- let nameLabel = UILabel(frame: CGRect.zero)
- nameLabel.numberOfLines = 0
- return nameLabel
- }()
-
- /// The disclosure image view
- public lazy var disclosureImageView: UIImageView = {
- let disclosureImage = UIImage.messageKitImageWith(type: .disclosure)?.withRenderingMode(.alwaysTemplate)
- let disclosure = UIImageView(image: disclosureImage)
- return disclosure
- }()
-
- // MARK: - Methods
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else {
- return
- }
- nameLabel.font = attributes.messageLabelFont
- }
+ // MARK: Open
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(initialsContainerView)
- messageContainerView.addSubview(nameLabel)
- messageContainerView.addSubview(disclosureImageView)
- initialsContainerView.addSubview(initialsLabel)
- setupConstraints()
+ // MARK: - Methods
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else {
+ return
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- nameLabel.text = ""
- initialsLabel.text = ""
+ nameLabel.font = attributes.messageLabelFont
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(initialsContainerView)
+ messageContainerView.addSubview(nameLabel)
+ messageContainerView.addSubview(disclosureImageView)
+ initialsContainerView.addSubview(initialsLabel)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ nameLabel.text = ""
+ initialsLabel.text = ""
+ }
+
+ open func setupConstraints() {
+ initialsContainerView.constraint(equalTo: CGSize(width: 26, height: 26))
+ let initialsConstraints = initialsContainerView.addConstraints(
+ left: messageContainerView.leftAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ leftConstant: 5)
+ initialsConstraints.first?.identifier = ConstraintsID.initialsContainerLeftConstraint.rawValue
+ initialsContainerView.layer.cornerRadius = 13
+ initialsLabel.fillSuperview()
+ disclosureImageView.constraint(equalTo: CGSize(width: 20, height: 20))
+ let disclosureConstraints = disclosureImageView.addConstraints(
+ right: messageContainerView.rightAnchor,
+ centerY: messageContainerView.centerYAnchor,
+ rightConstant: -10)
+ disclosureConstraints.first?.identifier = ConstraintsID.disclosureRightConstraint.rawValue
+ nameLabel.addConstraints(
+ messageContainerView.topAnchor,
+ left: initialsContainerView.rightAnchor,
+ bottom: messageContainerView.bottomAnchor,
+ right: disclosureImageView.leftAnchor,
+ topConstant: 0,
+ leftConstant: 10,
+ bottomConstant: 0,
+ rightConstant: 5)
+ }
+
+ // MARK: - Configure Cell
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+ // setup data
+ guard case .contact(let contactItem) = message.kind else { fatalError("Failed decorate audio cell") }
+ nameLabel.text = contactItem.displayName
+ initialsLabel.text = contactItem.initials
+ // setup constraints
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- open func setupConstraints() {
- initialsContainerView.constraint(equalTo: CGSize(width: 26, height: 26))
- let initialsConstraints = initialsContainerView.addConstraints(left: messageContainerView.leftAnchor, centerY: messageContainerView.centerYAnchor,
- leftConstant: 5)
- initialsConstraints.first?.identifier = ConstraintsID.initialsContainerLeftConstraint.rawValue
- initialsContainerView.layer.cornerRadius = 13
- initialsLabel.fillSuperview()
- disclosureImageView.constraint(equalTo: CGSize(width: 20, height: 20))
- let disclosureConstraints = disclosureImageView.addConstraints(right: messageContainerView.rightAnchor, centerY: messageContainerView.centerYAnchor,
- rightConstant: -10)
- disclosureConstraints.first?.identifier = ConstraintsID.disclosureRightConstraint.rawValue
- nameLabel.addConstraints(messageContainerView.topAnchor,
- left: initialsContainerView.rightAnchor,
- bottom: messageContainerView.bottomAnchor,
- right: disclosureImageView.leftAnchor,
- topConstant: 0,
- leftConstant: 10,
- bottomConstant: 0,
- rightConstant: 5)
+ let initialsContainerLeftConstraint = messageContainerView.constraints.filter { constraint -> Bool in
+ constraint.identifier == ConstraintsID.initialsContainerLeftConstraint.rawValue
+ }.first
+ let disclosureRightConstraint = messageContainerView.constraints.filter { constraint -> Bool in
+ constraint.identifier == ConstraintsID.disclosureRightConstraint.rawValue
+ }.first
+ if dataSource.isFromCurrentSender(message: message) { // outgoing message
+ initialsContainerLeftConstraint?.constant = 5
+ disclosureRightConstraint?.constant = -10
+ } else { // incoming message
+ initialsContainerLeftConstraint?.constant = 10
+ disclosureRightConstraint?.constant = -5
}
-
- // MARK: - Configure Cell
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
- // setup data
- guard case let .contact(contactItem) = message.kind else { fatalError("Failed decorate audio cell") }
- nameLabel.text = contactItem.displayName
- initialsLabel.text = contactItem.initials
- // setup constraints
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- let initialsContainerLeftConstraint = messageContainerView.constraints.filter { (constraint) -> Bool in
- return constraint.identifier == ConstraintsID.initialsContainerLeftConstraint.rawValue
- }.first
- let disclosureRightConstraint = messageContainerView.constraints.filter { (constraint) -> Bool in
- return constraint.identifier == ConstraintsID.disclosureRightConstraint.rawValue
- }.first
- if dataSource.isFromCurrentSender(message: message) { // outgoing message
- initialsContainerLeftConstraint?.constant = 5
- disclosureRightConstraint?.constant = -10
- } else { // incoming message
- initialsContainerLeftConstraint?.constant = 10
- disclosureRightConstraint?.constant = -5
- }
- // setup colors
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
- let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
- nameLabel.textColor = textColor
- disclosureImageView.tintColor = textColor
+ // setup colors
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
-
+ let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
+ nameLabel.textColor = textColor
+ disclosureImageView.tintColor = textColor
+ }
+
+ // MARK: Public
+
+ public enum ConstraintsID: String {
+ case initialsContainerLeftConstraint
+ case disclosureRightConstraint
+ }
+
+ /// The view container that holds contact initials
+ public lazy var initialsContainerView: UIView = {
+ let initialsContainer = UIView(frame: CGRect.zero)
+ initialsContainer.backgroundColor = .collectionViewBackground
+ return initialsContainer
+ }()
+
+ /// The label that display the contact initials
+ public lazy var initialsLabel: UILabel = {
+ let initialsLabel = UILabel(frame: CGRect.zero)
+ initialsLabel.textAlignment = .center
+ initialsLabel.textColor = .label
+ initialsLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
+ return initialsLabel
+ }()
+
+ /// The label that display contact name
+ public lazy var nameLabel: UILabel = {
+ let nameLabel = UILabel(frame: CGRect.zero)
+ nameLabel.numberOfLines = 0
+ return nameLabel
+ }()
+
+ /// The disclosure image view
+ public lazy var disclosureImageView: UIImageView = {
+ let disclosureImage = UIImage.messageKitImageWith(type: .disclosure)?.withRenderingMode(.alwaysTemplate)
+ let disclosure = UIImageView(image: disclosureImage)
+ return disclosure
+ }()
}
diff --git a/Sources/Views/Cells/LinkPreviewMessageCell.swift b/Sources/Views/Cells/LinkPreviewMessageCell.swift
index e14a0172c..6d2df58f7 100644
--- a/Sources/Views/Cells/LinkPreviewMessageCell.swift
+++ b/Sources/Views/Cells/LinkPreviewMessageCell.swift
@@ -1,96 +1,111 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class LinkPreviewMessageCell: TextMessageCell {
- public lazy var linkPreviewView: LinkPreviewView = {
- let view = LinkPreviewView()
- view.translatesAutoresizingMaskIntoConstraints = false
- messageContainerView.addSubview(view)
-
- NSLayoutConstraint.activate([
- view.leadingAnchor.constraint(equalTo: messageContainerView.leadingAnchor,
- constant: messageLabel.textInsets.left),
- view.trailingAnchor.constraint(equalTo: messageContainerView.trailingAnchor,
- constant: messageLabel.textInsets.right * -1),
- view.bottomAnchor.constraint(equalTo: messageContainerView.bottomAnchor,
- constant: messageLabel.textInsets.bottom * -1)
- ])
- return view
- }()
-
- private var linkURL: URL?
-
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
- linkPreviewView.titleLabel.font = attributes.linkPreviewFonts.titleFont
- linkPreviewView.teaserLabel.font = attributes.linkPreviewFonts.teaserFont
- linkPreviewView.domainLabel.font = attributes.linkPreviewFonts.domainFont
+ // MARK: Open
+
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
+ linkPreviewView.titleLabel.font = attributes.linkPreviewFonts.titleFont
+ linkPreviewView.teaserLabel.font = attributes.linkPreviewFonts.teaserFont
+ linkPreviewView.domainLabel.font = attributes.linkPreviewFonts.domainFont
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ let displayDelegate = messagesCollectionView.messagesDisplayDelegate
+
+ if let textColor: UIColor = displayDelegate?.textColor(for: message, at: indexPath, in: messagesCollectionView) {
+ linkPreviewView.titleLabel.textColor = textColor
+ linkPreviewView.teaserLabel.textColor = textColor
+ linkPreviewView.domainLabel.textColor = textColor
}
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- let displayDelegate = messagesCollectionView.messagesDisplayDelegate
-
- if let textColor: UIColor = displayDelegate?.textColor(for: message, at: indexPath, in: messagesCollectionView) {
- linkPreviewView.titleLabel.textColor = textColor
- linkPreviewView.teaserLabel.textColor = textColor
- linkPreviewView.domainLabel.textColor = textColor
- }
-
- guard case MessageKind.linkPreview(let linkItem) = message.kind else {
- fatalError("LinkPreviewMessageCell received unhandled MessageDataType: \(message.kind)")
- }
-
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
-
- linkPreviewView.titleLabel.text = linkItem.title
- linkPreviewView.teaserLabel.text = linkItem.teaser
- linkPreviewView.domainLabel.text = linkItem.url.host?.lowercased()
- linkPreviewView.imageView.image = linkItem.thumbnailImage
- linkURL = linkItem.url
-
- displayDelegate?.configureLinkPreviewImageView(linkPreviewView.imageView, for: message, at: indexPath, in: messagesCollectionView)
- }
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- linkPreviewView.titleLabel.text = nil
- linkPreviewView.teaserLabel.text = nil
- linkPreviewView.domainLabel.text = nil
- linkPreviewView.imageView.image = nil
- linkURL = nil
+ guard case MessageKind.linkPreview(let linkItem) = message.kind else {
+ fatalError("LinkPreviewMessageCell received unhandled MessageDataType: \(message.kind)")
}
- open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
- let touchLocation = gesture.location(in: linkPreviewView)
-
- guard linkPreviewView.frame.contains(touchLocation), let url = linkURL else {
- super.handleTapGesture(gesture)
- return
- }
- delegate?.didSelectURL(url)
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ linkPreviewView.titleLabel.text = linkItem.title
+ linkPreviewView.teaserLabel.text = linkItem.teaser
+ linkPreviewView.domainLabel.text = linkItem.url.host?.lowercased()
+ linkPreviewView.imageView.image = linkItem.thumbnailImage
+ linkURL = linkItem.url
+
+ displayDelegate?.configureLinkPreviewImageView(
+ linkPreviewView.imageView,
+ for: message,
+ at: indexPath,
+ in: messagesCollectionView)
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ linkPreviewView.titleLabel.text = nil
+ linkPreviewView.teaserLabel.text = nil
+ linkPreviewView.domainLabel.text = nil
+ linkPreviewView.imageView.image = nil
+ linkURL = nil
+ }
+
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: linkPreviewView)
+
+ guard linkPreviewView.frame.contains(touchLocation), let url = linkURL else {
+ super.handleTapGesture(gesture)
+ return
}
+ delegate?.didSelectURL(url)
+ }
+
+ // MARK: Public
+
+ public lazy var linkPreviewView: LinkPreviewView = {
+ let view = LinkPreviewView()
+ view.translatesAutoresizingMaskIntoConstraints = false
+ messageContainerView.addSubview(view)
+
+ NSLayoutConstraint.activate([
+ view.leadingAnchor.constraint(
+ equalTo: messageContainerView.leadingAnchor,
+ constant: messageLabel.textInsets.left),
+ view.trailingAnchor.constraint(
+ equalTo: messageContainerView.trailingAnchor,
+ constant: messageLabel.textInsets.right * -1),
+ view.bottomAnchor.constraint(
+ equalTo: messageContainerView.bottomAnchor,
+ constant: messageLabel.textInsets.bottom * -1),
+ ])
+ return view
+ }()
+
+ // MARK: Private
+
+ private var linkURL: URL?
}
diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift
index 23dcfb22f..44601b1b8 100644
--- a/Sources/Views/Cells/LocationMessageCell.swift
+++ b/Sources/Views/Cells/LocationMessageCell.swift
@@ -1,111 +1,126 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import UIKit
import MapKit
+import UIKit
/// A subclass of `MessageContentCell` used to display location messages.
open class LocationMessageCell: MessageContentCell {
-
- /// The activity indicator to be displayed while the map image is loading.
- open var activityIndicator = UIActivityIndicatorView(style: .gray)
-
- /// The image view holding the map image.
- open var imageView = UIImageView()
-
- private weak var snapShotter: MKMapSnapshotter?
-
- open override func setupSubviews() {
- super.setupSubviews()
- imageView.contentMode = .scaleAspectFill
- messageContainerView.addSubview(imageView)
- messageContainerView.addSubview(activityIndicator)
- setupConstraints()
+ // MARK: Open
+
+ /// The activity indicator to be displayed while the map image is loading.
+ open var activityIndicator = UIActivityIndicatorView(style: .medium)
+
+ /// The image view holding the map image.
+ open var imageView = UIImageView()
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ imageView.contentMode = .scaleAspectFill
+ messageContainerView.addSubview(imageView)
+ messageContainerView.addSubview(activityIndicator)
+ setupConstraints()
+ }
+
+ /// Responsible for setting up the constraints of the cell's subviews.
+ open func setupConstraints() {
+ imageView.fillSuperview()
+ activityIndicator.centerInSuperview()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ snapShotter?.cancel()
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard case .location(let locationItem) = message.kind else { fatalError("Configuring LocationMessageCell with wrong message kind") }
+ guard CLLocationCoordinate2DIsValid(locationItem.location.coordinate) else {
+ return
}
- /// Responsible for setting up the constraints of the cell's subviews.
- open func setupConstraints() {
- imageView.fillSuperview()
- activityIndicator.centerInSuperview()
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- snapShotter?.cancel()
+ let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
+ let annotationView = displayDelegate.annotationViewForLocation(
+ message: message,
+ at: indexPath,
+ in: messagesCollectionView)
+ let animationBlock = displayDelegate.animationBlockForLocation(
+ message: message,
+ at: indexPath,
+ in: messagesCollectionView)
+
+ activityIndicator.startAnimating()
+
+ let snapshotOptions = MKMapSnapshotter.Options()
+ snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span)
+ snapshotOptions.showsBuildings = options.showsBuildings
+ snapshotOptions.pointOfInterestFilter = options.showsPointsOfInterest ? .includingAll : .excludingAll
+
+ let snapShotter = MKMapSnapshotter(options: snapshotOptions)
+ self.snapShotter = snapShotter
+ snapShotter.start { snapshot, error in
+ defer {
+ self.activityIndicator.stopAnimating()
+ }
+ guard let snapshot = snapshot, error == nil else {
+ // show an error image?
+ return
+ }
+
+ guard let annotationView = annotationView else {
+ self.imageView.image = snapshot.image
+ return
+ }
+
+ UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0)
+
+ snapshot.image.draw(at: .zero)
+
+ var point = snapshot.point(for: locationItem.location.coordinate)
+ // Move point to reflect annotation anchor
+ point.x -= annotationView.bounds.size.width / 2
+ point.y -= annotationView.bounds.size.height / 2
+ point.x += annotationView.centerOffset.x
+ point.y += annotationView.centerOffset.y
+
+ annotationView.image?.draw(at: point)
+ let composedImage = UIGraphicsGetImageFromCurrentImageContext()
+
+ UIGraphicsEndImageContext()
+ self.imageView.image = composedImage
+ animationBlock?(self.imageView)
}
+ }
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
- let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
- let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView)
- let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView)
-
- guard case let .location(locationItem) = message.kind else { fatalError("") }
-
- activityIndicator.startAnimating()
-
- let snapshotOptions = MKMapSnapshotter.Options()
- snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span)
- snapshotOptions.showsBuildings = options.showsBuildings
- snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest
-
- let snapShotter = MKMapSnapshotter(options: snapshotOptions)
- self.snapShotter = snapShotter
- snapShotter.start { (snapshot, error) in
- defer {
- self.activityIndicator.stopAnimating()
- }
- guard let snapshot = snapshot, error == nil else {
- //show an error image?
- return
- }
-
- guard let annotationView = annotationView else {
- self.imageView.image = snapshot.image
- return
- }
-
- UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0)
-
- snapshot.image.draw(at: .zero)
-
- var point = snapshot.point(for: locationItem.location.coordinate)
- //Move point to reflect annotation anchor
- point.x -= annotationView.bounds.size.width / 2
- point.y -= annotationView.bounds.size.height / 2
- point.x += annotationView.centerOffset.x
- point.y += annotationView.centerOffset.y
-
- annotationView.image?.draw(at: point)
- let composedImage = UIGraphicsGetImageFromCurrentImageContext()
-
- UIGraphicsEndImageContext()
- self.imageView.image = composedImage
- animationBlock?(self.imageView)
- }
- }
+ // MARK: Private
+
+ private weak var snapShotter: MKMapSnapshotter?
}
diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift
index b66d8f8a0..f93779352 100644
--- a/Sources/Views/Cells/MediaMessageCell.swift
+++ b/Sources/Views/Cells/MediaMessageCell.swift
@@ -1,96 +1,96 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `MessageContentCell` used to display video and audio messages.
open class MediaMessageCell: MessageContentCell {
-
- /// The play button view to display on video messages.
- open lazy var playButtonView: PlayButtonView = {
- let playButtonView = PlayButtonView()
- return playButtonView
- }()
-
- /// The image view display the media content.
- open var imageView: UIImageView = {
- let imageView = UIImageView()
- imageView.contentMode = .scaleAspectFill
- return imageView
- }()
-
- // MARK: - Methods
-
- /// Responsible for setting up the constraints of the cell's subviews.
- open func setupConstraints() {
- imageView.fillSuperview()
- playButtonView.centerInSuperview()
- playButtonView.constraint(equalTo: CGSize(width: 35, height: 35))
+ /// The play button view to display on video messages.
+ open lazy var playButtonView: PlayButtonView = {
+ let playButtonView = PlayButtonView()
+ return playButtonView
+ }()
+
+ /// The image view display the media content.
+ open var imageView: UIImageView = {
+ let imageView = UIImageView()
+ imageView.contentMode = .scaleAspectFill
+ return imageView
+ }()
+
+ // MARK: - Methods
+
+ /// Responsible for setting up the constraints of the cell's subviews.
+ open func setupConstraints() {
+ imageView.fillSuperview()
+ playButtonView.centerInSuperview()
+ playButtonView.constraint(equalTo: CGSize(width: 35, height: 35))
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(imageView)
+ messageContainerView.addSubview(playButtonView)
+ setupConstraints()
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ imageView.image = nil
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(imageView)
- messageContainerView.addSubview(playButtonView)
- setupConstraints()
- }
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- self.imageView.image = nil
+ switch message.kind {
+ case .photo(let mediaItem):
+ imageView.image = mediaItem.image ?? mediaItem.placeholderImage
+ playButtonView.isHidden = true
+ case .video(let mediaItem):
+ imageView.image = mediaItem.image ?? mediaItem.placeholderImage
+ playButtonView.isHidden = false
+ default:
+ break
}
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
+ displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView)
+ }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
-
- switch message.kind {
- case .photo(let mediaItem):
- imageView.image = mediaItem.image ?? mediaItem.placeholderImage
- playButtonView.isHidden = true
- case .video(let mediaItem):
- imageView.image = mediaItem.image ?? mediaItem.placeholderImage
- playButtonView.isHidden = false
- default:
- break
- }
-
- 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: imageView)
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: imageView)
- guard imageView.frame.contains(touchLocation) else {
- super.handleTapGesture(gesture)
- return
- }
- delegate?.didTapImage(in: self)
+ guard imageView.frame.contains(touchLocation) else {
+ super.handleTapGesture(gesture)
+ return
}
-
+ delegate?.didTapImage(in: self)
+ }
}
diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift
index 51855a1cf..41cbd1f02 100644
--- a/Sources/Views/Cells/MessageCollectionViewCell.swift
+++ b/Sources/Views/Cells/MessageCollectionViewCell.swift
@@ -1,45 +1,45 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`.
open class MessageCollectionViewCell: UICollectionViewCell {
+ // MARK: Lifecycle
- // MARK: - Initializers
+ // MARK: - Initializers
- public override init(frame: CGRect) {
- super.init(frame: frame)
- }
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
- /// Handle tap gesture on contentView and its subviews.
- open func handleTapGesture(_ gesture: UIGestureRecognizer) {
- // Should be overridden
- }
+ // MARK: Open
+ /// Handle tap gesture on contentView and its subviews.
+ open func handleTapGesture(_: UIGestureRecognizer) {
+ // Should be overridden
+ }
}
diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift
index 618d0eaea..471f36c3b 100644
--- a/Sources/Views/Cells/MessageContentCell.swift
+++ b/Sources/Views/Cells/MessageContentCell.swift
@@ -1,365 +1,373 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
/// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages.
open class MessageContentCell: MessageCollectionViewCell {
-
- /// The image view displaying the avatar.
- open var avatarView = AvatarView()
-
- /// The container used for styling and holding the message's content view.
- open var messageContainerView: MessageContainerView = {
- let containerView = MessageContainerView()
- containerView.clipsToBounds = true
- containerView.layer.masksToBounds = true
- return containerView
- }()
-
- /// The top label of the cell.
- open var cellTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- label.textAlignment = .center
- return label
- }()
-
- /// The bottom label of the cell.
- open var cellBottomLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- label.textAlignment = .center
- return label
- }()
-
- /// The top label of the messageBubble.
- open var messageTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
-
- /// The bottom label of the messageBubble.
- open var messageBottomLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
-
- /// The time label of the messageBubble.
- open var messageTimestampLabel: InsetLabel = InsetLabel()
-
- // Should only add customized subviews - don't change accessoryView itself.
- open var accessoryView: UIView = UIView()
-
- /// The `MessageCellDelegate` for the cell.
- open weak var delegate: MessageCellDelegate?
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
- }
-
- open func setupSubviews() {
- contentView.addSubview(accessoryView)
- contentView.addSubview(cellTopLabel)
- contentView.addSubview(messageTopLabel)
- contentView.addSubview(messageBottomLabel)
- contentView.addSubview(cellBottomLabel)
- contentView.addSubview(messageContainerView)
- contentView.addSubview(avatarView)
- contentView.addSubview(messageTimestampLabel)
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ /// The image view displaying the avatar.
+ open var avatarView = AvatarView()
+
+ /// The container used for styling and holding the message's content view.
+ open var messageContainerView: MessageContainerView = {
+ let containerView = MessageContainerView()
+ containerView.clipsToBounds = true
+ containerView.layer.masksToBounds = true
+ return containerView
+ }()
+
+ /// The top label of the cell.
+ open var cellTopLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ label.textAlignment = .center
+ return label
+ }()
+
+ /// The bottom label of the cell.
+ open var cellBottomLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ label.textAlignment = .center
+ return label
+ }()
+
+ /// The top label of the messageBubble.
+ open var messageTopLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ return label
+ }()
+
+ /// The bottom label of the messageBubble.
+ open var messageBottomLabel: InsetLabel = {
+ let label = InsetLabel()
+ label.numberOfLines = 0
+ return label
+ }()
+
+ /// The time label of the messageBubble.
+ open var messageTimestampLabel = InsetLabel()
+
+ // Should only add customized subviews - don't change accessoryView itself.
+ open var accessoryView = UIView()
+
+ /// The `MessageCellDelegate` for the cell.
+ open weak var delegate: MessageCellDelegate?
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ cellTopLabel.text = nil
+ cellBottomLabel.text = nil
+ messageTopLabel.text = nil
+ messageBottomLabel.text = nil
+ messageTimestampLabel.attributedText = nil
+ }
+
+ open func setupSubviews() {
+ contentView.addSubviews(
+ accessoryView,
+ cellTopLabel,
+ messageTopLabel,
+ messageBottomLabel,
+ cellBottomLabel,
+ messageContainerView,
+ avatarView,
+ messageTimestampLabel)
+ }
+
+ // MARK: - Configuration
+
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
+ // Call this before other laying out other subviews
+ layoutMessageContainerView(with: attributes)
+ layoutMessageBottomLabel(with: attributes)
+ layoutCellBottomLabel(with: attributes)
+ layoutCellTopLabel(with: attributes)
+ layoutMessageTopLabel(with: attributes)
+ layoutAvatarView(with: attributes)
+ layoutAccessoryView(with: attributes)
+ layoutTimeLabelView(with: attributes)
+ }
+
+ /// Used to configure the cell.
+ ///
+ /// - Parameters:
+ /// - message: The `MessageType` this cell displays.
+ /// - indexPath: The `IndexPath` for this cell.
+ /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
+ open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
+ guard let dataSource = messagesCollectionView.messagesDataSource else {
+ fatalError(MessageKitError.nilMessagesDataSource)
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- cellTopLabel.text = nil
- cellBottomLabel.text = nil
- messageTopLabel.text = nil
- messageBottomLabel.text = nil
- messageTimestampLabel.attributedText = nil
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- // MARK: - Configuration
-
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
- // Call this before other laying out other subviews
- layoutMessageContainerView(with: attributes)
- layoutMessageBottomLabel(with: attributes)
- layoutCellBottomLabel(with: attributes)
- layoutCellTopLabel(with: attributes)
- layoutMessageTopLabel(with: attributes)
- layoutAvatarView(with: attributes)
- layoutAccessoryView(with: attributes)
- layoutTimeLabelView(with: attributes)
+ delegate = messagesCollectionView.messageCellDelegate
+
+ let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
+ let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
+
+ displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
+
+ displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
+
+ messageContainerView.backgroundColor = messageColor
+ messageContainerView.style = messageStyle
+
+ let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
+ let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
+ let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
+ let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
+ let messageTimestampLabelText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath)
+ cellTopLabel.attributedText = topCellLabelText
+ cellBottomLabel.attributedText = bottomCellLabelText
+ messageTopLabel.attributedText = topMessageLabelText
+ messageBottomLabel.attributedText = bottomMessageLabelText
+ messageTimestampLabel.attributedText = messageTimestampLabelText
+ messageTimestampLabel.isHidden = !messagesCollectionView.showMessageTimestampOnSwipeLeft
+ }
+
+ /// Handle tap gesture on contentView and its subviews.
+ open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ let touchLocation = gesture.location(in: self)
+
+ switch true {
+ case messageContainerView.frame
+ .contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
+ delegate?.didTapMessage(in: self)
+ case avatarView.frame.contains(touchLocation):
+ delegate?.didTapAvatar(in: self)
+ case cellTopLabel.frame.contains(touchLocation):
+ delegate?.didTapCellTopLabel(in: self)
+ case cellBottomLabel.frame.contains(touchLocation):
+ delegate?.didTapCellBottomLabel(in: self)
+ case messageTopLabel.frame.contains(touchLocation):
+ delegate?.didTapMessageTopLabel(in: self)
+ case messageBottomLabel.frame.contains(touchLocation):
+ delegate?.didTapMessageBottomLabel(in: self)
+ case accessoryView.frame.contains(touchLocation):
+ delegate?.didTapAccessoryView(in: self)
+ default:
+ delegate?.didTapBackground(in: self)
}
-
- /// Used to configure the cell.
- ///
- /// - Parameters:
- /// - message: The `MessageType` this cell displays.
- /// - indexPath: The `IndexPath` for this cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
- open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
-
- delegate = messagesCollectionView.messageCellDelegate
-
- let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
- let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
-
- displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
-
- displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
-
- messageContainerView.backgroundColor = messageColor
- messageContainerView.style = messageStyle
-
- let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
- let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
- let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
- let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
- let messageTimestampLabelText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath)
- cellTopLabel.attributedText = topCellLabelText
- cellBottomLabel.attributedText = bottomCellLabelText
- messageTopLabel.attributedText = topMessageLabelText
- messageBottomLabel.attributedText = bottomMessageLabelText
- messageTimestampLabel.attributedText = messageTimestampLabelText
- messageTimestampLabel.isHidden = !messagesCollectionView.showMessageTimestampOnSwipeLeft
+ }
+
+ /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
+ open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ let touchPoint = gestureRecognizer.location(in: self)
+ guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
+ return messageContainerView.frame.contains(touchPoint)
+ }
+
+ /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
+ open func cellContentView(canHandle _: CGPoint) -> Bool {
+ false
+ }
+
+ // MARK: - Origin Calculations
+
+ /// Positions the cell's `AvatarView`.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+ let padding = attributes.avatarLeadingTrailingPadding
+
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = padding
+ case .cellTrailing:
+ origin.x = attributes.frame.width - attributes.avatarSize.width - padding
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- /// Handle tap gesture on contentView and its subviews.
- open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
- let touchLocation = gesture.location(in: self)
-
- switch true {
- case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
- delegate?.didTapMessage(in: self)
- case avatarView.frame.contains(touchLocation):
- delegate?.didTapAvatar(in: self)
- case cellTopLabel.frame.contains(touchLocation):
- delegate?.didTapCellTopLabel(in: self)
- case cellBottomLabel.frame.contains(touchLocation):
- delegate?.didTapCellBottomLabel(in: self)
- case messageTopLabel.frame.contains(touchLocation):
- delegate?.didTapMessageTopLabel(in: self)
- case messageBottomLabel.frame.contains(touchLocation):
- delegate?.didTapMessageBottomLabel(in: self)
- case accessoryView.frame.contains(touchLocation):
- delegate?.didTapAccessoryView(in: self)
- default:
- delegate?.didTapBackground(in: self)
- }
+ switch attributes.avatarPosition.vertical {
+ case .messageLabelTop:
+ origin.y = messageTopLabel.frame.minY
+ case .messageTop: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.minY
+ case .messageBottom: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
+ case .messageCenter: // Needs messageContainerView frame to be set
+ origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height / 2)
+ case .cellBottom:
+ origin.y = attributes.frame.height - attributes.avatarSize.height
+ default:
+ break
}
- /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
- open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- let touchPoint = gestureRecognizer.location(in: self)
- guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
- return messageContainerView.frame.contains(touchPoint)
+ avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
+ }
+
+ /// Positions the cell's `MessageContainerView`.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+
+ switch attributes.avatarPosition.vertical {
+ case .messageBottom:
+ origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize
+ .height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes
+ .messageContainerPadding.top
+ case .messageCenter:
+ if attributes.avatarSize.height > attributes.messageContainerSize.height {
+ let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
+ origin.y = (attributes.size.height / 2) - (messageHeight / 2)
+ } else {
+ fallthrough
+ }
+ default:
+ if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
+ let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
+ origin.y = (attributes.size.height / 2) - (messageHeight / 2)
+ } else {
+ origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes
+ .messageContainerPadding.top
+ }
}
- /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
- open func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
- return false
+ let avatarPadding = attributes.avatarLeadingTrailingPadding
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding
+ case .cellTrailing:
+ origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes
+ .messageContainerPadding.right - avatarPadding
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- // MARK: - Origin Calculations
-
- /// Positions the cell's `AvatarView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
- let padding = attributes.avatarLeadingTrailingPadding
-
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = padding
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width - padding
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- switch attributes.avatarPosition.vertical {
- case .messageLabelTop:
- origin.y = messageTopLabel.frame.minY
- case .messageTop: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.minY
- case .messageBottom: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
- case .messageCenter: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2)
- case .cellBottom:
- origin.y = attributes.frame.height - attributes.avatarSize.height
- default:
- break
- }
-
- avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
+ messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
+ }
+
+ /// 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)
+ }
+
+ /// Positions the cell's bottom label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment
+ cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets
+
+ let y = messageBottomLabel.frame.maxY
+ let origin = CGPoint(x: 0, y: y)
+
+ cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize)
+ }
+
+ /// Positions the message bubble's top label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
+ messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
+
+ let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
+ let origin = CGPoint(x: 0, y: y)
+
+ messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
+ }
+
+ /// Positions the message bubble's bottom label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
+ messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
+ messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
+
+ let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
+ let origin = CGPoint(x: 0, y: y)
+
+ messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
+ }
+
+ /// Positions the cell's accessory view.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ var origin: CGPoint = .zero
+
+ // Accessory view is set at the side space of the messageContainerView
+ switch attributes.accessoryViewPosition {
+ case .messageLabelTop:
+ origin.y = messageTopLabel.frame.minY
+ case .messageTop:
+ origin.y = messageContainerView.frame.minY
+ case .messageBottom:
+ origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height
+ case .messageCenter:
+ origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
+ case .cellBottom:
+ origin.y = attributes.frame.height - attributes.accessoryViewSize.height
+ default:
+ break
}
- /// Positions the cell's `MessageContainerView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
-
- switch attributes.avatarPosition.vertical {
- case .messageBottom:
- origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize.height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes.messageContainerPadding.top
- case .messageCenter:
- if attributes.avatarSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- fallthrough
- }
- default:
- if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top
- }
- }
-
- let avatarPadding = attributes.avatarLeadingTrailingPadding
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - avatarPadding
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
+ // Accessory view is always on the opposite side of avatar
+ switch attributes.avatarPosition.horizontal {
+ case .cellLeading:
+ origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
+ case .cellTrailing:
+ origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize
+ .width
+ case .natural:
+ fatalError(MessageKitError.avatarPositionUnresolved)
}
- /// 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)
- }
-
- /// Positions the cell's bottom label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment
- cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets
-
- let y = messageBottomLabel.frame.maxY
- let origin = CGPoint(x: 0, y: y)
-
- cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize)
- }
-
- /// Positions the message bubble's top label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
- messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
-
- let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
- let origin = CGPoint(x: 0, y: y)
-
- messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
- }
-
- /// Positions the message bubble's bottom label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
- messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
-
- let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
- let origin = CGPoint(x: 0, y: y)
-
- messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
- }
-
- /// Positions the cell's accessory view.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
-
- var origin: CGPoint = .zero
-
- // Accessory view is set at the side space of the messageContainerView
- switch attributes.accessoryViewPosition {
- case .messageLabelTop:
- origin.y = messageTopLabel.frame.minY
- case .messageTop:
- origin.y = messageContainerView.frame.minY
- case .messageBottom:
- origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height
- case .messageCenter:
- origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
- case .cellBottom:
- origin.y = attributes.frame.height - attributes.accessoryViewSize.height
- default:
- break
- }
-
- // Accessory view is always on the opposite side of avatar
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
- case .cellTrailing:
- origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
-
- accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
- }
-
- /// Positions the message bubble's time label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- 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)
- let size = CGSize(width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height)
- messageTimestampLabel.frame = CGRect(origin: origin, size: size)
- }
+ accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
+ }
+
+ /// Positions the message bubble's time label.
+ /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
+ open func layoutTimeLabelView(with attributes: MessagesCollectionViewLayoutAttributes) {
+ let paddingLeft: CGFloat = 10
+ let origin = CGPoint(
+ 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/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift
index f79c1018f..08fcf381b 100644
--- a/Sources/Views/Cells/TextMessageCell.swift
+++ b/Sources/Views/Cells/TextMessageCell.swift
@@ -1,102 +1,102 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `MessageContentCell` used to display text messages.
open class TextMessageCell: MessageContentCell {
+ /// The label used to display the message's text.
+ open var messageLabel = MessageLabel()
- // MARK: - Properties
+ // MARK: - Properties
- /// The `MessageCellDelegate` for the cell.
- open override weak var delegate: MessageCellDelegate? {
- didSet {
- messageLabel.delegate = delegate
- }
+ /// The `MessageCellDelegate` for the cell.
+ open override weak var delegate: MessageCellDelegate? {
+ didSet {
+ messageLabel.delegate = delegate
}
+ }
- /// The label used to display the message's text.
- open var messageLabel = MessageLabel()
-
- // MARK: - Methods
+ // MARK: - Methods
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
- messageLabel.textInsets = attributes.messageLabelInsets
- messageLabel.messageLabelFont = attributes.messageLabelFont
- messageLabel.frame = messageContainerView.bounds
- }
+ open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+ super.apply(layoutAttributes)
+ if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
+ messageLabel.textInsets = attributes.messageLabelInsets
+ messageLabel.messageLabelFont = attributes.messageLabelFont
+ messageLabel.frame = messageContainerView.bounds
}
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- messageLabel.attributedText = nil
- messageLabel.text = nil
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ messageLabel.attributedText = nil
+ messageLabel.text = nil
+ }
+
+ open override func setupSubviews() {
+ super.setupSubviews()
+ messageContainerView.addSubview(messageLabel)
+ }
+
+ open override func configure(
+ with message: MessageType,
+ at indexPath: IndexPath,
+ and messagesCollectionView: MessagesCollectionView)
+ {
+ super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+ guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+ fatalError(MessageKitError.nilMessagesDisplayDelegate)
}
- open override func setupSubviews() {
- super.setupSubviews()
- messageContainerView.addSubview(messageLabel)
- }
-
- open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- super.configure(with: message, at: indexPath, and: messagesCollectionView)
-
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
+ let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
+
+ messageLabel.configure {
+ messageLabel.enabledDetectors = enabledDetectors
+ for detector in enabledDetectors {
+ let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
+ messageLabel.setAttributes(attributes, detector: detector)
+ }
+ let textMessageKind = message.kind.textMessageKind
+ switch textMessageKind {
+ case .text(let text), .emoji(let text):
+ let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
+ messageLabel.text = text
+ messageLabel.textColor = textColor
+ if let font = messageLabel.messageLabelFont {
+ messageLabel.font = font
}
-
- let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
-
- messageLabel.configure {
- messageLabel.enabledDetectors = enabledDetectors
- for detector in enabledDetectors {
- let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
- messageLabel.setAttributes(attributes, detector: detector)
- }
- let textMessageKind = message.kind.textMessageKind
- switch textMessageKind {
- case .text(let text), .emoji(let text):
- let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
- messageLabel.text = text
- messageLabel.textColor = textColor
- if let font = messageLabel.messageLabelFont {
- messageLabel.font = font
- }
- case .attributedText(let text):
- messageLabel.attributedText = text
- default:
- break
- }
- }
- }
-
- /// Used to handle the cell's contentView's tap gesture.
- /// Return false when the contentView does not need to handle the gesture.
- open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
- return messageLabel.handleGesture(touchPoint)
+ case .attributedText(let text):
+ messageLabel.attributedText = text
+ default:
+ break
+ }
}
+ }
+ /// Used to handle the cell's contentView's tap gesture.
+ /// Return false when the contentView does not need to handle the gesture.
+ open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
+ messageLabel.handleGesture(touchPoint)
+ }
}
diff --git a/Sources/Views/Cells/TypingIndicatorCell.swift b/Sources/Views/Cells/TypingIndicatorCell.swift
index 7e8c5d948..368d25b50 100644
--- a/Sources/Views/Cells/TypingIndicatorCell.swift
+++ b/Sources/Views/Cells/TypingIndicatorCell.swift
@@ -1,66 +1,62 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `MessageCollectionViewCell` used to display the typing indicator.
open class TypingIndicatorCell: MessageCollectionViewCell {
-
- // MARK: - Subviews
+ // MARK: Lifecycle
- public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0)
-
- public let typingBubble = TypingBubble()
-
- // MARK: - Initialization
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupSubviews()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupSubviews()
- }
-
- open func setupSubviews() {
- addSubview(typingBubble)
- }
-
- open override func prepareForReuse() {
- super.prepareForReuse()
- if typingBubble.isAnimating {
- typingBubble.stopAnimating()
- }
- }
-
- // MARK: - Layout
-
- open override func layoutSubviews() {
- super.layoutSubviews()
- typingBubble.frame = bounds.inset(by: insets)
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupSubviews()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ open func setupSubviews() {
+ addSubview(typingBubble)
+ }
+
+ open override func prepareForReuse() {
+ super.prepareForReuse()
+ if typingBubble.isAnimating {
+ typingBubble.stopAnimating()
}
-
+ }
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ typingBubble.frame = bounds.inset(by: insets)
+ }
+
+ // MARK: Public
+
+ public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0)
+
+ public let typingBubble = TypingBubble()
}
diff --git a/Sources/Views/HeadersFooters/MessageReusableView.swift b/Sources/Views/HeadersFooters/MessageReusableView.swift
index d8becac6c..77654ed72 100644
--- a/Sources/Views/HeadersFooters/MessageReusableView.swift
+++ b/Sources/Views/HeadersFooters/MessageReusableView.swift
@@ -1,40 +1,36 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class MessageReusableView: UICollectionReusableView {
+ // MARK: - Initializers
- // MARK: - Initializers
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- }
-
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
}
diff --git a/Sources/Views/InsetLabel.swift b/Sources/Views/InsetLabel.swift
index 5cdfa1729..9e6c7b62f 100644
--- a/Sources/Views/InsetLabel.swift
+++ b/Sources/Views/InsetLabel.swift
@@ -1,38 +1,34 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class InsetLabel: UILabel {
-
- open var textInsets: UIEdgeInsets = .zero {
- didSet { setNeedsDisplay() }
- }
-
- open override func drawText(in rect: CGRect) {
- let insetRect = rect.inset(by: textInsets)
- super.drawText(in: insetRect)
- }
-
+ open var textInsets: UIEdgeInsets = .zero {
+ didSet { setNeedsDisplay() }
+ }
+
+ open override func drawText(in rect: CGRect) {
+ let insetRect = rect.inset(by: textInsets)
+ super.drawText(in: insetRect)
+ }
}
diff --git a/Sources/Views/LinkPreviewView.swift b/Sources/Views/LinkPreviewView.swift
index 22b51c6c3..28fc1b1c1 100644
--- a/Sources/Views/LinkPreviewView.swift
+++ b/Sources/Views/LinkPreviewView.swift
@@ -1,112 +1,120 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class LinkPreviewView: UIView {
- lazy var imageView: UIImageView = {
- let imageView: UIImageView = .init()
- imageView.clipsToBounds = true
- imageView.contentMode = .scaleAspectFill
- imageView.translatesAutoresizingMaskIntoConstraints = false
-
- addSubview(imageView)
-
- NSLayoutConstraint.activate([
- imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
- imageView.topAnchor.constraint(equalTo: topAnchor),
- imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1),
- imageView.widthAnchor.constraint(equalToConstant: LinkPreviewMessageSizeCalculator.imageViewSize),
- imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)
- ])
-
- return imageView
- }()
- lazy var titleLabel: UILabel = {
- let label: UILabel = .init()
- label.numberOfLines = 0
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
- lazy var teaserLabel: UILabel = {
- let label: UILabel = .init()
- label.numberOfLines = 0
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
- lazy var domainLabel: UILabel = {
- let label: UILabel = .init()
- label.numberOfLines = 0
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- private lazy var contentView: UIView = {
- let view: UIView = .init(frame: .zero)
- view.translatesAutoresizingMaskIntoConstraints = false
-
- addSubview(view)
-
- NSLayoutConstraint.activate([
- view.topAnchor.constraint(equalTo: topAnchor),
- view.leadingAnchor.constraint(equalTo: imageView.trailingAnchor,
- constant: LinkPreviewMessageSizeCalculator.imageViewMargin),
- view.trailingAnchor.constraint(equalTo: trailingAnchor),
- view.bottomAnchor.constraint(equalTo: bottomAnchor)
- ])
-
- return view
- }()
-
- init() {
- super.init(frame: .zero)
-
- contentView.addSubview(titleLabel)
- NSLayoutConstraint.activate([
- titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
- titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
- titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
- ])
-
- contentView.addSubview(teaserLabel)
- NSLayoutConstraint.activate([
- teaserLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
- teaserLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
- teaserLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
- ])
- teaserLabel.setContentHuggingPriority(.init(249), for: .vertical)
-
- contentView.addSubview(domainLabel)
- NSLayoutConstraint.activate([
- domainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
- domainLabel.topAnchor.constraint(equalTo: teaserLabel.bottomAnchor, constant: 3),
- domainLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
- domainLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
- ])
- }
-
- required public init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
+ // MARK: Lifecycle
+
+ init() {
+ super.init(frame: .zero)
+
+ contentView.addSubview(titleLabel)
+ NSLayoutConstraint.activate([
+ titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
+ titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ ])
+
+ contentView.addSubview(teaserLabel)
+ NSLayoutConstraint.activate([
+ teaserLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ teaserLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
+ teaserLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ ])
+ teaserLabel.setContentHuggingPriority(.init(249), for: .vertical)
+
+ contentView.addSubview(domainLabel)
+ NSLayoutConstraint.activate([
+ domainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ domainLabel.topAnchor.constraint(equalTo: teaserLabel.bottomAnchor, constant: 3),
+ domainLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ domainLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+ ])
+ }
+
+ required public init?(coder _: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: Internal
+
+ lazy var imageView: UIImageView = {
+ let imageView: UIImageView = .init()
+ imageView.clipsToBounds = true
+ imageView.contentMode = .scaleAspectFill
+ imageView.translatesAutoresizingMaskIntoConstraints = false
+
+ addSubview(imageView)
+
+ NSLayoutConstraint.activate([
+ imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ imageView.topAnchor.constraint(equalTo: topAnchor),
+ imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1),
+ imageView.widthAnchor.constraint(equalToConstant: LinkPreviewMessageSizeCalculator.imageViewSize),
+ imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
+ ])
+
+ return imageView
+ }()
+
+ lazy var titleLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ lazy var teaserLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ lazy var domainLabel: UILabel = {
+ let label: UILabel = .init()
+ label.numberOfLines = 0
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ // MARK: Private
+
+ private lazy var contentView: UIView = {
+ let view: UIView = .init(frame: .zero)
+ view.translatesAutoresizingMaskIntoConstraints = false
+
+ addSubview(view)
+
+ NSLayoutConstraint.activate([
+ view.topAnchor.constraint(equalTo: topAnchor),
+ view.leadingAnchor.constraint(
+ equalTo: imageView.trailingAnchor,
+ constant: LinkPreviewMessageSizeCalculator.imageViewMargin),
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ return view
+ }()
}
diff --git a/Sources/Views/MessageContainerView.swift b/Sources/Views/MessageContainerView.swift
index cbecffa4f..e816f0d19 100644
--- a/Sources/Views/MessageContainerView.swift
+++ b/Sources/Views/MessageContainerView.swift
@@ -1,88 +1,89 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class MessageContainerView: UIImageView {
+ // MARK: Open
- // MARK: - Properties
-
- private let imageMask = UIImageView()
-
- open var style: MessageStyle = .none {
- didSet {
- applyMessageStyle()
- }
+ open var style: MessageStyle = .none {
+ didSet {
+ applyMessageStyle()
}
+ }
- open override var frame: CGRect {
- didSet {
- sizeMaskToView()
- }
+ open override var frame: CGRect {
+ didSet {
+ sizeMaskToView()
}
+ }
+
+ // MARK: Private
+
+ // MARK: - Properties
+
+ private let imageMask = UIImageView()
- // MARK: - Methods
+ // MARK: - Methods
- private func sizeMaskToView() {
- switch style {
- case .none, .custom:
- break
- case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline:
- imageMask.frame = bounds
- }
+ private func sizeMaskToView() {
+ switch style {
+ case .none, .custom:
+ break
+ case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline, .customImageTail:
+ imageMask.frame = bounds
}
+ }
- private func applyMessageStyle() {
- switch style {
- case .bubble, .bubbleTail:
- imageMask.image = style.image
- sizeMaskToView()
- mask = imageMask
- image = nil
- case .bubbleOutline(let color):
- let bubbleStyle: MessageStyle = .bubble
- imageMask.image = bubbleStyle.image
- sizeMaskToView()
- mask = imageMask
- image = style.image?.withRenderingMode(.alwaysTemplate)
- tintColor = color
- case .bubbleTailOutline(let color, let tail, let corner):
- let bubbleStyle: MessageStyle = .bubbleTail(tail, corner)
- imageMask.image = bubbleStyle.image
- sizeMaskToView()
- mask = imageMask
- image = style.image?.withRenderingMode(.alwaysTemplate)
- tintColor = color
- case .none:
- mask = nil
- image = nil
- tintColor = nil
- case .custom(let configurationClosure):
- mask = nil
- image = nil
- tintColor = nil
- configurationClosure(self)
- }
+ private func applyMessageStyle() {
+ switch style {
+ case .bubble, .bubbleTail, .customImageTail:
+ imageMask.image = style.image
+ sizeMaskToView()
+ mask = imageMask
+ image = nil
+ case .bubbleOutline(let color):
+ let bubbleStyle: MessageStyle = .bubble
+ imageMask.image = bubbleStyle.image
+ sizeMaskToView()
+ mask = imageMask
+ image = style.image?.withRenderingMode(.alwaysTemplate)
+ tintColor = color
+ case .bubbleTailOutline(let color, let tail, let corner):
+ let bubbleStyle: MessageStyle = .bubbleTail(tail, corner)
+ imageMask.image = bubbleStyle.image
+ sizeMaskToView()
+ mask = imageMask
+ image = style.image?.withRenderingMode(.alwaysTemplate)
+ tintColor = color
+ case .none:
+ mask = nil
+ image = nil
+ tintColor = nil
+ case .custom(let configurationClosure):
+ mask = nil
+ image = nil
+ tintColor = nil
+ configurationClosure(self)
}
+ }
}
diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift
index 90a78c05f..036bc3565 100644
--- a/Sources/Views/MessageLabel.swift
+++ b/Sources/Views/MessageLabel.swift
@@ -1,551 +1,581 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
+// MARK: - MessageLabel
+
open class MessageLabel: UILabel {
+ // MARK: Lifecycle
- // MARK: - Private Properties
-
- private lazy var layoutManager: NSLayoutManager = {
- let layoutManager = NSLayoutManager()
- layoutManager.addTextContainer(self.textContainer)
- return layoutManager
- }()
-
- private lazy var textContainer: NSTextContainer = {
- let textContainer = NSTextContainer()
- textContainer.lineFragmentPadding = 0
- textContainer.maximumNumberOfLines = self.numberOfLines
- textContainer.lineBreakMode = self.lineBreakMode
- textContainer.size = self.bounds.size
- return textContainer
- }()
-
- private lazy var textStorage: NSTextStorage = {
- let textStorage = NSTextStorage()
- textStorage.addLayoutManager(self.layoutManager)
- return textStorage
- }()
-
- internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
-
- private var isConfiguring: Bool = false
+ // MARK: - Initializers
- // MARK: - Public Properties
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupView()
+ }
- open weak var delegate: MessageLabelDelegate?
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupView()
+ }
- open var enabledDetectors: [DetectorType] = [] {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
- }
+ // MARK: Open
- open override var attributedText: NSAttributedString? {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
- }
+ // MARK: - Public Properties
- open override var text: String? {
- didSet {
- setTextStorage(attributedText, shouldParse: true)
- }
- }
+ open weak var delegate: MessageLabelDelegate?
- open override var font: UIFont! {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
- }
+ open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- open override var textColor: UIColor! {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
- }
-
- open override var lineBreakMode: NSLineBreakMode {
- didSet {
- textContainer.lineBreakMode = lineBreakMode
- if !isConfiguring { setNeedsDisplay() }
- }
- }
-
- open override var numberOfLines: Int {
- didSet {
- textContainer.maximumNumberOfLines = numberOfLines
- if !isConfiguring { setNeedsDisplay() }
- }
- }
-
- open override var textAlignment: NSTextAlignment {
- didSet {
- setTextStorage(attributedText, shouldParse: false)
- }
- }
-
- open var textInsets: UIEdgeInsets = .zero {
- didSet {
- if !isConfiguring { setNeedsDisplay() }
- }
- }
+ open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- open override var intrinsicContentSize: CGSize {
- var size = super.intrinsicContentSize
- size.width += textInsets.horizontal
- size.height += textInsets.vertical
- return size
- }
-
- internal var messageLabelFont: UIFont?
+ open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- private var attributesNeedUpdate = false
+ open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- public static var defaultAttributes: [NSAttributedString.Key: Any] = {
- return [
- NSAttributedString.Key.foregroundColor: UIColor.darkText,
- NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
- NSAttributedString.Key.underlineColor: UIColor.darkText
- ]
- }()
+ open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes
- open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+ open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]
- open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes
-
- open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]
-
- public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
- switch detector {
- case .phoneNumber:
- phoneNumberAttributes = attributes
- case .address:
- addressAttributes = attributes
- case .date:
- dateAttributes = attributes
- case .url:
- urlAttributes = attributes
- case .transitInformation:
- transitInformationAttributes = attributes
- case .mention:
- mentionAttributes = attributes
- case .hashtag:
- hashtagAttributes = attributes
- case .custom(let regex):
- customAttributes[regex] = attributes
- }
- if isConfiguring {
- attributesNeedUpdate = true
- } else {
- updateAttributes(for: [detector])
- }
+ open var enabledDetectors: [DetectorType] = [] {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- // MARK: - Initializers
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupView()
+ open override var attributedText: NSAttributedString? {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupView()
+ open override var text: String? {
+ didSet {
+ setTextStorage(attributedText, shouldParse: true)
}
+ }
- // MARK: - Open Methods
-
- open override func drawText(in rect: CGRect) {
-
- let insetRect = rect.inset(by: textInsets)
- textContainer.size = CGSize(width: insetRect.width, height: rect.height)
-
- let origin = insetRect.origin
- let range = layoutManager.glyphRange(for: textContainer)
-
- layoutManager.drawBackground(forGlyphRange: range, at: origin)
- layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
+ // swiftlint:disable:next implicitly_unwrapped_optional
+ open override var font: UIFont! {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- // MARK: - Public Methods
-
- public func configure(block: () -> Void) {
- isConfiguring = true
- block()
- if attributesNeedUpdate {
- updateAttributes(for: enabledDetectors)
- }
- attributesNeedUpdate = false
- isConfiguring = false
- setNeedsDisplay()
+ // swiftlint:disable:next implicitly_unwrapped_optional
+ open override var textColor: UIColor! {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- // MARK: - Private Methods
-
- private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
-
- guard let newText = newText, newText.length > 0 else {
- textStorage.setAttributedString(NSAttributedString())
- setNeedsDisplay()
- return
- }
-
- let style = paragraphStyle(for: newText)
- let range = NSRange(location: 0, length: newText.length)
-
- let mutableText = NSMutableAttributedString(attributedString: newText)
- mutableText.addAttribute(.paragraphStyle, value: style, range: range)
-
- if shouldParse {
- rangesForDetectors.removeAll()
- let results = parse(text: mutableText)
- setRangesForDetectors(in: results)
- }
-
- for (detector, rangeTuples) in rangesForDetectors {
- if enabledDetectors.contains(detector) {
- let attributes = detectorAttributes(for: detector)
- rangeTuples.forEach { (range, _) in
- mutableText.addAttributes(attributes, range: range)
- }
- }
- }
-
- let modifiedText = NSAttributedString(attributedString: mutableText)
- textStorage.setAttributedString(modifiedText)
-
- if !isConfiguring { setNeedsDisplay() }
-
+ open override var lineBreakMode: NSLineBreakMode {
+ didSet {
+ textContainer.lineBreakMode = lineBreakMode
+ if !isConfiguring { setNeedsDisplay() }
}
-
- private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
- guard text.length > 0 else { return NSParagraphStyle() }
-
- var range = NSRange(location: 0, length: text.length)
- let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
- let style = existingStyle ?? NSMutableParagraphStyle()
-
- style.lineBreakMode = lineBreakMode
- style.alignment = textAlignment
-
- return style
- }
-
- private func updateAttributes(for detectors: [DetectorType]) {
-
- guard let attributedText = attributedText, attributedText.length > 0 else { return }
- let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
-
- for detector in detectors {
- 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)
- }
-
- let updatedString = NSAttributedString(attributedString: mutableAttributedString)
- textStorage.setAttributedString(updatedString)
- }
+ open override var numberOfLines: Int {
+ didSet {
+ textContainer.maximumNumberOfLines = numberOfLines
+ if !isConfiguring { setNeedsDisplay() }
}
+ }
- private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
-
- switch detectorType {
- case .address:
- return addressAttributes
- case .date:
- return dateAttributes
- case .phoneNumber:
- return phoneNumberAttributes
- case .url:
- return urlAttributes
- case .transitInformation:
- return transitInformationAttributes
- case .mention:
- return mentionAttributes
- case .hashtag:
- return hashtagAttributes
- case .custom(let regex):
- return customAttributes[regex] ?? MessageLabel.defaultAttributes
- }
-
+ open override var textAlignment: NSTextAlignment {
+ didSet {
+ setTextStorage(attributedText, shouldParse: false)
}
+ }
- private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
- switch checkingResultType {
- case .address:
- return addressAttributes
- case .date:
- return dateAttributes
- case .phoneNumber:
- return phoneNumberAttributes
- case .link:
- return urlAttributes
- case .transitInformation:
- return transitInformationAttributes
- default:
- fatalError(MessageKitError.unrecognizedCheckingResult)
- }
- }
-
- private func setupView() {
- numberOfLines = 0
- lineBreakMode = .byWordWrapping
+ open var textInsets: UIEdgeInsets = .zero {
+ didSet {
+ if !isConfiguring { setNeedsDisplay() }
}
+ }
- // MARK: - Parsing Text
-
- private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
- guard enabledDetectors.isEmpty == false else { return [] }
- let range = NSRange(location: 0, length: text.length)
- var matches = [NSTextCheckingResult]()
-
- // Get matches of all .custom DetectorType and add it to matches array
- let regexs = enabledDetectors
- .filter { $0.isCustom }
- .map { parseForMatches(with: $0, in: text, for: range) }
- .joined()
- matches.append(contentsOf: regexs)
-
- // Get all Checking Types of detectors, except for .custom because they contain their own regex
- let detectorCheckingTypes = enabledDetectors
- .filter { !$0.isCustom }
- .reduce(0) { $0 | $1.textCheckingType.rawValue }
- if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
- let detectorMatches = detector.matches(in: text.string, options: [], range: range)
- matches.append(contentsOf: detectorMatches)
- }
+ open override var intrinsicContentSize: CGSize {
+ var size = super.intrinsicContentSize
+ size.width += textInsets.horizontal
+ size.height += textInsets.vertical
+ return size
+ }
- guard enabledDetectors.contains(.url) else {
- return matches
- }
+ // MARK: - Open Methods
- // Enumerate NSAttributedString NSLinks and append ranges
- var results: [NSTextCheckingResult] = matches
+ open override func drawText(in rect: CGRect) {
+ let insetRect = rect.inset(by: textInsets)
+ textContainer.size = CGSize(width: insetRect.width, height: insetRect.height)
- text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
- guard let url = value as? URL else { return }
- let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
- results.append(result)
- }
+ let origin = insetRect.origin
+ let range = layoutManager.glyphRange(for: textContainer)
- return results
- }
+ layoutManager.drawBackground(forGlyphRange: range, at: origin)
+ layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
+ }
- private func parseForMatches(with detector: DetectorType, in text: NSAttributedString, for range: NSRange) -> [NSTextCheckingResult] {
- switch detector {
- case .custom(let regex):
- return regex.matches(in: text.string, options: [], range: range)
- default:
- fatalError("You must pass a .custom DetectorType")
- }
- }
-
- private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
-
- guard checkingResults.isEmpty == false else { return }
-
- for result in checkingResults {
-
- switch result.resultType {
- case .address:
- var ranges = rangesForDetectors[.address] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .address)
- case .date:
- var ranges = rangesForDetectors[.date] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .date)
- case .phoneNumber:
- var ranges = rangesForDetectors[.phoneNumber] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
- case .link:
- var ranges = rangesForDetectors[.url] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .url)
- case .transitInformation:
- var ranges = rangesForDetectors[.transitInformation] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
- case .regularExpression:
- guard let text = text, let regex = result.regularExpression, let range = Range(result.range, in: text) else { return }
- let detector = DetectorType.custom(regex)
- var ranges = rangesForDetectors[detector] ?? []
- let tuple: (NSRange, MessageTextCheckingType) = (result.range, .custom(pattern: regex.pattern, match: String(text[range])))
- ranges.append(tuple)
- rangesForDetectors.updateValue(ranges, forKey: detector)
- default:
- fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
- }
+ open func handleGesture(_ touchLocation: CGPoint) -> Bool {
+ guard let index = stringIndex(at: touchLocation) else { return false }
+ for (detectorType, ranges) in rangesForDetectors {
+ for (range, value) in ranges {
+ if range.contains(index) {
+ handleGesture(for: detectorType, value: value)
+ return true
}
-
+ }
+ }
+ return false
+ }
+
+ // MARK: Public
+
+ public static var defaultAttributes: [NSAttributedString.Key: Any] = {
+ [
+ NSAttributedString.Key.foregroundColor: UIColor.darkText,
+ NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
+ NSAttributedString.Key.underlineColor: UIColor.darkText,
+ ]
+ }()
+
+ public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
+ switch detector {
+ case .phoneNumber:
+ phoneNumberAttributes = attributes
+ case .address:
+ addressAttributes = attributes
+ case .date:
+ dateAttributes = attributes
+ case .url:
+ urlAttributes = attributes
+ case .transitInformation:
+ transitInformationAttributes = attributes
+ case .mention:
+ mentionAttributes = attributes
+ case .hashtag:
+ hashtagAttributes = attributes
+ case .custom(let regex):
+ customAttributes[regex] = attributes
+ }
+ if isConfiguring {
+ attributesNeedUpdate = true
+ } else {
+ updateAttributes(for: [detector])
+ }
+ }
+
+ // MARK: - Public Methods
+
+ public func configure(block: () -> Void) {
+ isConfiguring = true
+ block()
+ if attributesNeedUpdate {
+ updateAttributes(for: enabledDetectors)
+ }
+ attributesNeedUpdate = false
+ isConfiguring = false
+ setNeedsDisplay()
+ }
+
+ // MARK: Internal
+
+ internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
+
+ internal var messageLabelFont: UIFont?
+
+ // MARK: Private
+
+ // MARK: - Private Properties
+
+ private lazy var layoutManager: NSLayoutManager = {
+ let layoutManager = NSLayoutManager()
+ layoutManager.addTextContainer(self.textContainer)
+ return layoutManager
+ }()
+
+ private lazy var textContainer: NSTextContainer = {
+ let textContainer = NSTextContainer()
+ textContainer.lineFragmentPadding = 0
+ textContainer.maximumNumberOfLines = self.numberOfLines
+ textContainer.lineBreakMode = self.lineBreakMode
+ textContainer.size = self.bounds.size
+ return textContainer
+ }()
+
+ private lazy var textStorage: NSTextStorage = {
+ let textStorage = NSTextStorage()
+ textStorage.addLayoutManager(self.layoutManager)
+ return textStorage
+ }()
+
+ private var isConfiguring = false
+
+ private var attributesNeedUpdate = false
+
+ // MARK: - Private Methods
+
+ private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
+ guard let newText = newText, newText.length > 0 else {
+ textStorage.setAttributedString(NSAttributedString())
+ setNeedsDisplay()
+ return
+ }
+
+ let style = paragraphStyle(for: newText)
+ let range = NSRange(location: 0, length: newText.length)
+
+ let mutableText = NSMutableAttributedString(attributedString: newText)
+ mutableText.addAttribute(.paragraphStyle, value: style, range: range)
+
+ if shouldParse {
+ rangesForDetectors.removeAll()
+ let results = parse(text: mutableText)
+ setRangesForDetectors(in: results)
+ }
+
+ for (detector, rangeTuples) in rangesForDetectors {
+ if enabledDetectors.contains(detector) {
+ let attributes = detectorAttributes(for: detector)
+ rangeTuples.forEach { range, _ in
+ mutableText.addAttributes(attributes, range: range)
+ }
+ }
}
- // MARK: - Gesture Handling
+ let modifiedText = NSAttributedString(attributedString: mutableText)
+ textStorage.setAttributedString(modifiedText)
- private func stringIndex(at location: CGPoint) -> Int? {
- guard textStorage.length > 0 else { return nil }
+ if !isConfiguring { setNeedsDisplay() }
+ }
- var location = location
+ private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
+ guard text.length > 0 else { return NSParagraphStyle() }
- location.x -= textInsets.left
- location.y -= textInsets.top
+ var range = NSRange(location: 0, length: text.length)
+ let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
+ let style = existingStyle ?? NSMutableParagraphStyle()
- let index = layoutManager.glyphIndex(for: location, in: textContainer)
+ style.lineBreakMode = lineBreakMode
+ style.alignment = textAlignment
- let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
-
- var characterIndex: Int?
-
- if lineRect.contains(location) {
- characterIndex = layoutManager.characterIndexForGlyph(at: index)
- }
-
- return characterIndex
-
- }
+ return style
+ }
- open func handleGesture(_ touchLocation: CGPoint) -> Bool {
+ private func updateAttributes(for detectors: [DetectorType]) {
+ guard let attributedText = attributedText, attributedText.length > 0 else { return }
+ let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
- guard let index = stringIndex(at: touchLocation) else { return false }
+ for detector in detectors {
+ guard let rangeTuples = rangesForDetectors[detector] else { continue }
- for (detectorType, ranges) in rangesForDetectors {
- for (range, value) in ranges {
- if range.contains(index) {
- handleGesture(for: detectorType, value: value)
- return true
- }
- }
+ 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)
}
- return false
- }
- // swiftlint:disable cyclomatic_complexity
- private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
-
- switch value {
- case let .addressComponents(addressComponents):
- var transformedAddressComponents = [String: String]()
- guard let addressComponents = addressComponents else { return }
- addressComponents.forEach { (key, value) in
- transformedAddressComponents[key.rawValue] = value
- }
- handleAddress(transformedAddressComponents)
- case let .phoneNumber(phoneNumber):
- guard let phoneNumber = phoneNumber else { return }
- handlePhoneNumber(phoneNumber)
- case let .date(date):
- guard let date = date else { return }
- handleDate(date)
- case let .link(url):
- guard let url = url else { return }
- handleURL(url)
- case let .transitInfoComponents(transitInformation):
- var transformedTransitInformation = [String: String]()
- guard let transitInformation = transitInformation else { return }
- transitInformation.forEach { (key, value) in
- transformedTransitInformation[key.rawValue] = value
- }
- handleTransitInformation(transformedTransitInformation)
- case let .custom(pattern, match):
- guard let match = match else { return }
- switch detectorType {
- case .hashtag:
- handleHashtag(match)
- case .mention:
- handleMention(match)
- default:
- handleCustom(pattern, match: match)
- }
- }
- }
- // swiftlint:enable cyclomatic_complexity
-
- private func handleAddress(_ addressComponents: [String: String]) {
- delegate?.didSelectAddress(addressComponents)
- }
-
- private func handleDate(_ date: Date) {
- delegate?.didSelectDate(date)
- }
+ let attributes = detectorAttributes(for: detector)
+ mutableAttributedString.addAttributes(attributes, range: range)
+ }
+
+ let updatedString = NSAttributedString(attributedString: mutableAttributedString)
+ textStorage.setAttributedString(updatedString)
+ }
+ }
+
+ private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
+ switch detectorType {
+ case .address:
+ return addressAttributes
+ case .date:
+ return dateAttributes
+ case .phoneNumber:
+ return phoneNumberAttributes
+ case .url:
+ return urlAttributes
+ case .transitInformation:
+ return transitInformationAttributes
+ case .mention:
+ return mentionAttributes
+ case .hashtag:
+ return hashtagAttributes
+ case .custom(let regex):
+ return customAttributes[regex] ?? MessageLabel.defaultAttributes
+ }
+ }
+
+ private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
+ switch checkingResultType {
+ case .address:
+ return addressAttributes
+ case .date:
+ return dateAttributes
+ case .phoneNumber:
+ return phoneNumberAttributes
+ case .link:
+ return urlAttributes
+ case .transitInformation:
+ return transitInformationAttributes
+ default:
+ fatalError(MessageKitError.unrecognizedCheckingResult)
+ }
+ }
+
+ private func setupView() {
+ numberOfLines = 0
+ lineBreakMode = .byWordWrapping
+ }
+
+ // MARK: - Parsing Text
+
+ private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
+ guard enabledDetectors.isEmpty == false else { return [] }
+ let range = NSRange(location: 0, length: text.length)
+ var matches = [NSTextCheckingResult]()
+
+ // Get matches of all .custom DetectorType and add it to matches array
+ let regexs = enabledDetectors
+ .filter { $0.isCustom }
+ .map { parseForMatches(with: $0, in: text, for: range) }
+ .joined()
+ matches.append(contentsOf: removeOverlappingResults(Array(regexs)))
+
+ // Get all Checking Types of detectors, except for .custom because they contain their own regex
+ let detectorCheckingTypes = enabledDetectors
+ .filter { !$0.isCustom }
+ .reduce(0) { $0 | $1.textCheckingType.rawValue }
+ if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
+ let detectorMatches = detector.matches(in: text.string, options: [], range: range)
+ matches.append(contentsOf: detectorMatches)
+ }
+
+ guard enabledDetectors.contains(.url) else {
+ return matches
+ }
+
+ // Enumerate NSAttributedString NSLinks and append ranges
+ var results: [NSTextCheckingResult] = matches
+
+ text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
+ guard let url = value as? URL else { return }
+ let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
+ results.append(result)
+ }
+
+ return results
+ }
+
+ private func parseForMatches(
+ with detector: DetectorType,
+ in text: NSAttributedString,
+ for range: NSRange) -> [NSTextCheckingResult]
+ {
+ switch detector {
+ case .custom(let regex):
+ return regex.matches(in: text.string, options: [], range: range)
+ default:
+ fatalError("You must pass a .custom DetectorType")
+ }
+ }
+
+ private func removeOverlappingResults(_ results: [NSTextCheckingResult]) -> [NSTextCheckingResult]
+ {
+ var filteredResults: [NSTextCheckingResult] = []
- private func handleURL(_ url: URL) {
- delegate?.didSelectURL(url)
+ 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)
+ }
}
- private func handlePhoneNumber(_ phoneNumber: String) {
- delegate?.didSelectPhoneNumber(phoneNumber)
- }
-
- private func handleTransitInformation(_ components: [String: String]) {
- delegate?.didSelectTransitInformation(components)
- }
-
- private func handleHashtag(_ hashtag: String) {
- delegate?.didSelectHashtag(hashtag)
- }
-
- private func handleMention(_ mention: String) {
- delegate?.didSelectMention(mention)
- }
-
- private func handleCustom(_ pattern: String, match: String) {
- delegate?.didSelectCustom(pattern, match: match)
- }
-
+ return filteredResults
+ }
+
+ private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
+ guard checkingResults.isEmpty == false else { return }
+
+ for result in checkingResults {
+ switch result.resultType {
+ case .address:
+ var ranges = rangesForDetectors[.address] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .address)
+ case .date:
+ var ranges = rangesForDetectors[.date] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .date)
+ case .phoneNumber:
+ var ranges = rangesForDetectors[.phoneNumber] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
+ case .link:
+ var ranges = rangesForDetectors[.url] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .url)
+ case .transitInformation:
+ var ranges = rangesForDetectors[.transitInformation] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
+ case .regularExpression:
+ guard
+ let text = text, let regex = result.regularExpression,
+ let range = Range(result.range, in: text) else { return }
+ let detector = DetectorType.custom(regex)
+ var ranges = rangesForDetectors[detector] ?? []
+ let tuple: (NSRange, MessageTextCheckingType) = (
+ result.range,
+ .custom(pattern: regex.pattern, match: String(text[range])))
+ ranges.append(tuple)
+ rangesForDetectors.updateValue(ranges, forKey: detector)
+ default:
+ fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
+ }
+ }
+ }
+
+ // MARK: - Gesture Handling
+
+ private func stringIndex(at location: CGPoint) -> Int? {
+ guard textStorage.length > 0 else { return nil }
+
+ var location = location
+
+ location.x -= textInsets.left
+ location.y -= textInsets.top
+
+ let index = layoutManager.glyphIndex(for: location, in: textContainer)
+
+ let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
+
+ var characterIndex: Int?
+
+ if lineRect.contains(location) {
+ characterIndex = layoutManager.characterIndexForGlyph(at: index)
+ }
+
+ return characterIndex
+ }
+
+ // swiftlint:disable cyclomatic_complexity
+ private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) {
+ switch value {
+ case .addressComponents(let addressComponents):
+ var transformedAddressComponents = [String: String]()
+ guard let addressComponents = addressComponents else { return }
+ addressComponents.forEach { key, value in
+ transformedAddressComponents[key.rawValue] = value
+ }
+ handleAddress(transformedAddressComponents)
+ case .phoneNumber(let phoneNumber):
+ guard let phoneNumber = phoneNumber else { return }
+ handlePhoneNumber(phoneNumber)
+ case .date(let date):
+ guard let date = date else { return }
+ handleDate(date)
+ case .link(let url):
+ guard let url = url else { return }
+ handleURL(url)
+ case .transitInfoComponents(let transitInformation):
+ var transformedTransitInformation = [String: String]()
+ guard let transitInformation = transitInformation else { return }
+ transitInformation.forEach { key, value in
+ transformedTransitInformation[key.rawValue] = value
+ }
+ handleTransitInformation(transformedTransitInformation)
+ case .custom(let pattern, let match):
+ guard let match = match else { return }
+ switch detectorType {
+ case .hashtag:
+ handleHashtag(match)
+ case .mention:
+ handleMention(match)
+ default:
+ handleCustom(pattern, match: match)
+ }
+ }
+ }
+
+ // swiftlint:enable cyclomatic_complexity
+
+ private func handleAddress(_ addressComponents: [String: String]) {
+ delegate?.didSelectAddress(addressComponents)
+ }
+
+ private func handleDate(_ date: Date) {
+ delegate?.didSelectDate(date)
+ }
+
+ private func handleURL(_ url: URL) {
+ delegate?.didSelectURL(url)
+ }
+
+ private func handlePhoneNumber(_ phoneNumber: String) {
+ delegate?.didSelectPhoneNumber(phoneNumber)
+ }
+
+ private func handleTransitInformation(_ components: [String: String]) {
+ delegate?.didSelectTransitInformation(components)
+ }
+
+ private func handleHashtag(_ hashtag: String) {
+ delegate?.didSelectHashtag(hashtag)
+ }
+
+ private func handleMention(_ mention: String) {
+ delegate?.didSelectMention(mention)
+ }
+
+ private func handleCustom(_ pattern: String, match: String) {
+ delegate?.didSelectCustom(pattern, match: match)
+ }
}
+// MARK: - MessageTextCheckingType
+
internal enum MessageTextCheckingType {
- case addressComponents([NSTextCheckingKey: String]?)
- case date(Date?)
- case phoneNumber(String?)
- case link(URL?)
- case transitInfoComponents([NSTextCheckingKey: String]?)
- case custom(pattern: String, match: String?)
+ case addressComponents([NSTextCheckingKey: String]?)
+ case date(Date?)
+ case phoneNumber(String?)
+ case link(URL?)
+ case transitInfoComponents([NSTextCheckingKey: String]?)
+ case custom(pattern: String, match: String?)
}
diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift
index bbf5de975..a551ef2f9 100644
--- a/Sources/Views/MessagesCollectionView.swift
+++ b/Sources/Views/MessagesCollectionView.swift
@@ -1,217 +1,225 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import UIKit
open class MessagesCollectionView: UICollectionView {
+ // MARK: Lifecycle
- // MARK: - Properties
+ // MARK: - Initializers
- open weak var messagesDataSource: MessagesDataSource?
-
- open weak var messagesDisplayDelegate: MessagesDisplayDelegate?
-
- open weak var messagesLayoutDelegate: MessagesLayoutDelegate?
-
- open weak var messageCellDelegate: MessageCellDelegate?
-
- open var isTypingIndicatorHidden: Bool {
- return messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden
- }
-
- /// Display the date of message by swiping left.
- /// The default value of this property is `false`.
- internal var showMessageTimestampOnSwipeLeft: Bool = false
-
- private var indexPathForLastItem: IndexPath? {
- let lastSection = numberOfSections - 1
- guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil }
- return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection)
- }
-
- open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout {
- guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else {
- fatalError(MessageKitError.layoutUsedOnForeignType)
- }
- return layout
- }
-
- // MARK: - Initializers
-
- public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
- super.init(frame: frame, collectionViewLayout: layout)
- backgroundColor = .collectionViewBackground
- registerReusableViews()
- setupGestureRecognizers()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
- }
-
- public convenience init() {
- self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
- }
-
- // MARK: - Methods
-
- private func registerReusableViews() {
- register(TextMessageCell.self)
- register(MediaMessageCell.self)
- register(LocationMessageCell.self)
- register(AudioMessageCell.self)
- register(ContactMessageCell.self)
- register(TypingIndicatorCell.self)
- register(LinkPreviewMessageCell.self)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter)
- }
-
- private func setupGestureRecognizers() {
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
- tapGesture.delaysTouchesBegan = true
- addGestureRecognizer(tapGesture)
- }
-
- @objc
- open func handleTapGesture(_ gesture: UIGestureRecognizer) {
- guard gesture.state == .ended else { return }
-
- let touchLocation = gesture.location(in: self)
- guard let indexPath = indexPathForItem(at: touchLocation) else { return }
-
- let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell
- 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
- @available(*, deprecated, message: "Scroll to bottom by using scrollToLastItem(:) instead", renamed: "scrollToLastItem")
- public func scrollToBottom(animated: Bool = false) {
- 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)
- }
- }
-
- public func reloadDataAndKeepOffset() {
- // stop scrolling
- setContentOffset(contentOffset, animated: false)
-
- // calculate the offset and reloadData
- let beforeContentSize = contentSize
- reloadData()
- layoutIfNeeded()
- let afterContentSize = contentSize
-
- // reset the contentOffset after data is updated
- let newOffset = CGPoint(
- x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
- y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
- setContentOffset(newOffset, animated: false)
- }
-
- // MARK: - Typing Indicator API
-
- /// Notifies the layout that the typing indicator will change state
- ///
- /// - Parameters:
- /// - isHidden: A Boolean value that is to be the new state of the typing indicator
- internal func setTypingIndicatorViewHidden(_ isHidden: Bool) {
- messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden)
- }
-
- /// A method that by default checks if the section is the last in the
- /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
- /// is FALSE
- ///
- /// - Parameter section
- /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
- public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
- return messagesCollectionViewFlowLayout.isSectionReservedForTypingIndicator(section)
- }
-
- // MARK: - View Register/Dequeue
-
- /// Registers a particular cell using its reuse-identifier
- public func register(_ cellClass: T.Type) {
- register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
- }
-
- /// Registers a reusable view for a specific SectionKind
- public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) {
- register(reusableViewClass,
- forSupplementaryViewOfKind: kind,
- withReuseIdentifier: String(describing: T.self))
- }
-
- /// Registers a nib with reusable view for a specific SectionKind
- public func register(_ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil), headerFooterClassOfNib headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) {
- register(nib,
- forSupplementaryViewOfKind: kind,
- withReuseIdentifier: String(describing: T.self))
- }
-
- /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T {
- guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else {
- fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))")
- }
- return cell
- }
-
- /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
- let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath)
- guard let viewType = view as? T else {
- fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
- }
- return viewType
- }
-
- /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
- let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath)
- guard let viewType = view as? T else {
- fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
- }
- return viewType
- }
+ public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
+ super.init(frame: frame, collectionViewLayout: layout)
+ backgroundColor = .collectionViewBackground
+ registerReusableViews()
+ setupGestureRecognizers()
+ }
+ required public init?(coder _: NSCoder) {
+ super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
+ }
+
+ public convenience init() {
+ self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
+ }
+
+ // MARK: Open
+
+ // MARK: - Properties
+
+ open weak var messagesDataSource: MessagesDataSource?
+
+ open weak var messagesDisplayDelegate: MessagesDisplayDelegate?
+
+ open weak var messagesLayoutDelegate: MessagesLayoutDelegate?
+
+ open weak var messageCellDelegate: MessageCellDelegate?
+
+ open var isTypingIndicatorHidden: Bool {
+ messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden
+ }
+
+ open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout {
+ guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else {
+ fatalError(MessageKitError.layoutUsedOnForeignType)
+ }
+ return layout
+ }
+
+ @objc
+ open func handleTapGesture(_ gesture: UIGestureRecognizer) {
+ guard gesture.state == .ended else { return }
+
+ let touchLocation = gesture.location(in: self)
+ guard let indexPath = indexPathForItem(at: touchLocation) else { return }
+
+ let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell
+ cell?.handleTapGesture(gesture)
+ }
+
+ // MARK: Public
+
+ // 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 let indexPath = indexPathForLastItem else { return }
+
+ scrollToItem(at: indexPath, at: pos, animated: animated)
+ }
+
+ public func reloadDataAndKeepOffset() {
+ // stop scrolling
+ setContentOffset(contentOffset, animated: false)
+
+ // calculate the offset and reloadData
+ let beforeContentSize = contentSize
+ reloadData()
+ layoutIfNeeded()
+ let afterContentSize = contentSize
+
+ // reset the contentOffset after data is updated
+ let newOffset = CGPoint(
+ x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
+ y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
+ setContentOffset(newOffset, animated: false)
+ }
+
+ /// A method that by default checks if the section is the last in the
+ /// `messagesCollectionView` and that `isTypingIndicatorViewHidden`
+ /// is FALSE
+ ///
+ /// - Parameter section
+ /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section
+ public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool {
+ messagesCollectionViewFlowLayout.isSectionReservedForTypingIndicator(section)
+ }
+
+ // MARK: - View Register/Dequeue
+
+ /// Registers a particular cell using its reuse-identifier
+ public func register(_ cellClass: T.Type) {
+ register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Registers a reusable view for a specific SectionKind
+ public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) {
+ register(
+ reusableViewClass,
+ forSupplementaryViewOfKind: kind,
+ withReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Registers a nib with reusable view for a specific SectionKind
+ public func register(
+ _ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil),
+ headerFooterClassOfNib _: T.Type,
+ forSupplementaryViewOfKind kind: String)
+ {
+ register(
+ nib,
+ forSupplementaryViewOfKind: kind,
+ withReuseIdentifier: String(describing: T.self))
+ }
+
+ /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
+ public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T {
+ guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else {
+ fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))")
+ }
+ return cell
+ }
+
+ /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
+ public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
+ let view = dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionHeader,
+ withReuseIdentifier: String(describing: T.self),
+ for: indexPath)
+ guard let viewType = view as? T else {
+ fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
+ }
+ return viewType
+ }
+
+ /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
+ public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
+ let view = dequeueReusableSupplementaryView(
+ ofKind: UICollectionView.elementKindSectionFooter,
+ withReuseIdentifier: String(describing: T.self),
+ for: indexPath)
+ guard let viewType = view as? T else {
+ fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
+ }
+ return viewType
+ }
+
+ // MARK: Internal
+
+ /// Display the date of message by swiping left.
+ /// The default value of this property is `false`.
+ internal var showMessageTimestampOnSwipeLeft = false
+
+ // MARK: - Typing Indicator API
+
+ /// Notifies the layout that the typing indicator will change state
+ ///
+ /// - Parameters:
+ /// - isHidden: A Boolean value that is to be the new state of the typing indicator
+ internal func setTypingIndicatorViewHidden(_ isHidden: Bool) {
+ messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden)
+ }
+
+ // MARK: Private
+
+ private var indexPathForLastItem: IndexPath? {
+ guard numberOfSections > 0 else { return nil }
+
+ for offset in 1 ... numberOfSections {
+ let section = numberOfSections - offset
+ let lastItem = numberOfItems(inSection: section) - 1
+ if lastItem >= 0 {
+ return IndexPath(item: lastItem, section: section)
+ }
+ }
+ return nil
+ }
+
+ // MARK: - Methods
+
+ private func registerReusableViews() {
+ register(TextMessageCell.self)
+ register(MediaMessageCell.self)
+ register(LocationMessageCell.self)
+ register(AudioMessageCell.self)
+ register(ContactMessageCell.self)
+ register(TypingIndicatorCell.self)
+ register(LinkPreviewMessageCell.self)
+ register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader)
+ register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter)
+ }
+
+ private func setupGestureRecognizers() {
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
+ tapGesture.delaysTouchesBegan = true
+ addGestureRecognizer(tapGesture)
+ }
}
diff --git a/Sources/Views/MessagesInputContainerView.swift b/Sources/Views/MessagesInputContainerView.swift
new file mode 100644
index 000000000..1ff0ba533
--- /dev/null
+++ b/Sources/Views/MessagesInputContainerView.swift
@@ -0,0 +1,26 @@
+// MIT License
+//
+// Copyright (c) 2017-2022 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import Foundation
+import UIKit
+
+public final class MessagesInputContainerView: UIView { }
diff --git a/Sources/Views/PlayButtonView.swift b/Sources/Views/PlayButtonView.swift
index aba215d41..c78d96671 100644
--- a/Sources/Views/PlayButtonView.swift
+++ b/Sources/Views/PlayButtonView.swift
@@ -1,132 +1,136 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
open class PlayButtonView: UIView {
+ // MARK: Lifecycle
- // MARK: - Properties
-
- public let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
- public let triangleView = UIView()
-
- private var triangleCenterXConstraint: NSLayoutConstraint?
- private var cacheFrame: CGRect = .zero
-
- // MARK: - Initializers
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
-
- setupSubviews()
- setupConstraints()
- setupView()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
-
- setupSubviews()
- setupConstraints()
- setupView()
- }
-
- // MARK: - Methods
-
- open override func layoutSubviews() {
- super.layoutSubviews()
-
- guard !cacheFrame.equalTo(frame) else { return }
- cacheFrame = frame
-
- updateTriangleConstraints()
- applyCornerRadius()
- applyTriangleMask()
- }
-
- private func setupSubviews() {
- addSubview(blurView)
- addSubview(triangleView)
- }
-
- private func setupView() {
- triangleView.clipsToBounds = true
- triangleView.backgroundColor = .black
- blurView.clipsToBounds = true
- backgroundColor = .clear
- }
-
- private func setupConstraints() {
- triangleView.translatesAutoresizingMaskIntoConstraints = false
-
- let centerX = triangleView.centerXAnchor.constraint(equalTo: centerXAnchor)
- let centerY = triangleView.centerYAnchor.constraint(equalTo: centerYAnchor)
- let width = triangleView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5)
- let height = triangleView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5)
-
- triangleCenterXConstraint = centerX
-
- NSLayoutConstraint.activate([centerX, centerY, width, height])
-
- blurView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- blurView.centerXAnchor.constraint(equalTo: centerXAnchor),
- blurView.centerYAnchor.constraint(equalTo: centerYAnchor),
- blurView.heightAnchor.constraint(equalTo: heightAnchor),
- blurView.widthAnchor.constraint(equalTo: widthAnchor)
- ])
- }
-
- private func triangleMask(for frame: CGRect) -> CAShapeLayer {
- let shapeLayer = CAShapeLayer()
- let trianglePath = UIBezierPath()
-
- let point1 = CGPoint(x: frame.minX, y: frame.minY)
- let point2 = CGPoint(x: frame.maxX, y: frame.maxY/2)
- let point3 = CGPoint(x: frame.minX, y: frame.maxY)
-
- trianglePath .move(to: point1)
- trianglePath .addLine(to: point2)
- trianglePath .addLine(to: point3)
- trianglePath .close()
-
- shapeLayer.path = trianglePath.cgPath
-
- return shapeLayer
- }
-
- private func updateTriangleConstraints() {
- triangleCenterXConstraint?.constant = triangleView.frame.width / 8
- }
-
- private func applyTriangleMask() {
- let rect = CGRect(origin: .zero, size: triangleView.bounds.size)
- triangleView.layer.mask = triangleMask(for: rect)
- }
-
- private func applyCornerRadius() {
- blurView.layer.cornerRadius = frame.width / 2
- }
-
+ // MARK: - Initializers
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ setupSubviews()
+ setupConstraints()
+ setupView()
+ }
+
+ required public init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+
+ setupSubviews()
+ setupConstraints()
+ setupView()
+ }
+
+ // MARK: Open
+
+ // MARK: - Methods
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+
+ guard !cacheFrame.equalTo(frame) else { return }
+ cacheFrame = frame
+
+ updateTriangleConstraints()
+ applyCornerRadius()
+ applyTriangleMask()
+ }
+
+ // MARK: Public
+
+ // MARK: - Properties
+
+ public let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
+ public let triangleView = UIView()
+
+ // MARK: Private
+
+ private var triangleCenterXConstraint: NSLayoutConstraint?
+ private var cacheFrame: CGRect = .zero
+
+ private func setupSubviews() {
+ addSubview(blurView)
+ addSubview(triangleView)
+ }
+
+ private func setupView() {
+ triangleView.clipsToBounds = true
+ triangleView.backgroundColor = .black
+ blurView.clipsToBounds = true
+ backgroundColor = .clear
+ }
+
+ private func setupConstraints() {
+ triangleView.translatesAutoresizingMaskIntoConstraints = false
+
+ let centerX = triangleView.centerXAnchor.constraint(equalTo: centerXAnchor)
+ let centerY = triangleView.centerYAnchor.constraint(equalTo: centerYAnchor)
+ let width = triangleView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5)
+ let height = triangleView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5)
+
+ triangleCenterXConstraint = centerX
+
+ NSLayoutConstraint.activate([centerX, centerY, width, height])
+
+ blurView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ blurView.centerXAnchor.constraint(equalTo: centerXAnchor),
+ blurView.centerYAnchor.constraint(equalTo: centerYAnchor),
+ blurView.heightAnchor.constraint(equalTo: heightAnchor),
+ blurView.widthAnchor.constraint(equalTo: widthAnchor),
+ ])
+ }
+
+ private func triangleMask(for frame: CGRect) -> CAShapeLayer {
+ let shapeLayer = CAShapeLayer()
+ let trianglePath = UIBezierPath()
+
+ let point1 = CGPoint(x: frame.minX, y: frame.minY)
+ let point2 = CGPoint(x: frame.maxX, y: frame.maxY / 2)
+ let point3 = CGPoint(x: frame.minX, y: frame.maxY)
+
+ trianglePath.move(to: point1)
+ trianglePath.addLine(to: point2)
+ trianglePath.addLine(to: point3)
+ trianglePath.close()
+
+ shapeLayer.path = trianglePath.cgPath
+
+ return shapeLayer
+ }
+
+ private func updateTriangleConstraints() {
+ triangleCenterXConstraint?.constant = triangleView.frame.width / 8
+ }
+
+ private func applyTriangleMask() {
+ let rect = CGRect(origin: .zero, size: triangleView.bounds.size)
+ triangleView.layer.mask = triangleMask(for: rect)
+ }
+
+ private func applyCornerRadius() {
+ blurView.layer.cornerRadius = frame.width / 2
+ }
}
diff --git a/Sources/Views/TypingBubble.swift b/Sources/Views/TypingBubble.swift
index 004b7beb5..aef03025b 100644
--- a/Sources/Views/TypingBubble.swift
+++ b/Sources/Views/TypingBubble.swift
@@ -1,155 +1,173 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A subclass of `UIView` that mimics the iMessage typing bubble
open class TypingBubble: UIView {
-
- // MARK: - Properties
-
- open var isPulseEnabled: Bool = true
-
- public private(set) var isAnimating: Bool = false
-
- open override var backgroundColor: UIColor? {
- set {
- [contentBubble, cornerBubble, tinyBubble].forEach { $0.backgroundColor = newValue }
- }
- get {
- return contentBubble.backgroundColor
- }
- }
-
- private struct AnimationKeys {
- static let pulse = "typingBubble.pulse"
- }
-
- // MARK: - Subviews
-
- /// The indicator used to display the typing animation.
- public let typingIndicator = TypingIndicator()
-
- public let contentBubble = UIView()
-
- public let cornerBubble = BubbleCircle()
-
- public let tinyBubble = BubbleCircle()
-
- // MARK: - Animation Layers
-
- open var contentPulseAnimationLayer: CABasicAnimation {
- let animation = CABasicAnimation(keyPath: "transform.scale")
- animation.fromValue = 1
- animation.toValue = 1.04
- animation.duration = 1
- animation.repeatCount = .infinity
- animation.autoreverses = true
- return animation
- }
-
- open var circlePulseAnimationLayer: CABasicAnimation {
- let animation = CABasicAnimation(keyPath: "transform.scale")
- animation.fromValue = 1
- animation.toValue = 1.1
- animation.duration = 0.5
- animation.repeatCount = .infinity
- animation.autoreverses = true
- return animation
- }
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupSubviews()
- }
-
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupSubviews()
- }
-
- open func setupSubviews() {
- addSubview(tinyBubble)
- addSubview(cornerBubble)
- addSubview(contentBubble)
- contentBubble.addSubview(typingIndicator)
- backgroundColor = .incomingMessageBackground
- }
-
- // MARK: - Layout
-
- open override func layoutSubviews() {
- super.layoutSubviews()
-
- // To maintain the iMessage like bubble the width:height ratio of the frame
- // must be close to 1.65
- let ratio = bounds.width / bounds.height
- let extraRightInset = bounds.width - 1.65/ratio*bounds.width
-
- let tinyBubbleRadius: CGFloat = bounds.height / 6
- tinyBubble.frame = CGRect(x: 0,
- y: bounds.height - tinyBubbleRadius,
- width: tinyBubbleRadius,
- height: tinyBubbleRadius)
-
- let cornerBubbleRadius = tinyBubbleRadius * 2
- let offset: CGFloat = tinyBubbleRadius / 6
- cornerBubble.frame = CGRect(x: tinyBubbleRadius - offset,
- y: bounds.height - (1.5 * cornerBubbleRadius) + offset,
- width: cornerBubbleRadius,
- height: cornerBubbleRadius)
-
- let contentBubbleFrame = CGRect(x: tinyBubbleRadius + offset,
- y: 0,
- width: bounds.width - (tinyBubbleRadius + offset) - extraRightInset,
- height: bounds.height - (tinyBubbleRadius + offset))
- let contentBubbleFrameCornerRadius = contentBubbleFrame.height / 2
-
- contentBubble.frame = contentBubbleFrame
- contentBubble.layer.cornerRadius = contentBubbleFrameCornerRadius
-
- let insets = UIEdgeInsets(top: offset, left: contentBubbleFrameCornerRadius / 1.25, bottom: offset, right: contentBubbleFrameCornerRadius / 1.25)
- typingIndicator.frame = contentBubble.bounds.inset(by: insets)
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupSubviews()
+ }
+
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupSubviews()
+ }
+
+ // MARK: Open
+
+ // MARK: - Properties
+
+ open var isPulseEnabled = true
+
+ open override var backgroundColor: UIColor? {
+ set {
+ [contentBubble, cornerBubble, tinyBubble].forEach { $0.backgroundColor = newValue }
}
-
- // MARK: - Animation API
-
- open func startAnimating() {
- defer { isAnimating = true }
- guard !isAnimating else { return }
- typingIndicator.startAnimating()
- if isPulseEnabled {
- contentBubble.layer.add(contentPulseAnimationLayer, forKey: AnimationKeys.pulse)
- [cornerBubble, tinyBubble].forEach { $0.layer.add(circlePulseAnimationLayer, forKey: AnimationKeys.pulse) }
- }
+ get {
+ contentBubble.backgroundColor
}
-
- open func stopAnimating() {
- defer { isAnimating = false }
- guard isAnimating else { return }
- typingIndicator.stopAnimating()
- [contentBubble, cornerBubble, tinyBubble].forEach { $0.layer.removeAnimation(forKey: AnimationKeys.pulse) }
+ }
+
+ // MARK: - Animation Layers
+
+ open var contentPulseAnimationLayer: CABasicAnimation {
+ let animation = CABasicAnimation(keyPath: "transform.scale")
+ animation.fromValue = 1
+ animation.toValue = 1.04
+ animation.duration = 1
+ animation.repeatCount = .infinity
+ animation.autoreverses = true
+ return animation
+ }
+
+ open var circlePulseAnimationLayer: CABasicAnimation {
+ let animation = CABasicAnimation(keyPath: "transform.scale")
+ animation.fromValue = 1
+ animation.toValue = 1.1
+ animation.duration = 0.5
+ animation.repeatCount = .infinity
+ animation.autoreverses = true
+ return animation
+ }
+
+ open func setupSubviews() {
+ addSubview(tinyBubble)
+ addSubview(cornerBubble)
+ addSubview(contentBubble)
+ contentBubble.addSubview(typingIndicator)
+ backgroundColor = .incomingMessageBackground
+ }
+
+ // MARK: - Layout
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+
+ // To maintain the iMessage like bubble the width:height ratio of the frame
+ // must be close to 1.65
+
+ // In order to prevent NaN crash when assigning the frame of the contentBubble
+ guard
+ bounds.width > 0,
+ bounds.height > 0
+ else { return }
+
+ let ratio = bounds.width / bounds.height
+ let extraRightInset = bounds.width - 1.65 / ratio * bounds.width
+
+ let tinyBubbleRadius: CGFloat = bounds.height / 6
+ tinyBubble.frame = CGRect(
+ x: 0,
+ y: bounds.height - tinyBubbleRadius,
+ width: tinyBubbleRadius,
+ height: tinyBubbleRadius)
+
+ let cornerBubbleRadius = tinyBubbleRadius * 2
+ let offset: CGFloat = tinyBubbleRadius / 6
+ cornerBubble.frame = CGRect(
+ x: tinyBubbleRadius - offset,
+ y: bounds.height - (1.5 * cornerBubbleRadius) + offset,
+ width: cornerBubbleRadius,
+ height: cornerBubbleRadius)
+
+ let contentBubbleFrame = CGRect(
+ x: tinyBubbleRadius + offset,
+ y: 0,
+ width: bounds.width - (tinyBubbleRadius + offset) - extraRightInset,
+ height: bounds.height - (tinyBubbleRadius + offset))
+ let contentBubbleFrameCornerRadius = contentBubbleFrame.height / 2
+
+ contentBubble.frame = contentBubbleFrame
+ contentBubble.layer.cornerRadius = contentBubbleFrameCornerRadius
+
+ let insets = UIEdgeInsets(
+ top: offset,
+ left: contentBubbleFrameCornerRadius / 1.25,
+ bottom: offset,
+ right: contentBubbleFrameCornerRadius / 1.25)
+ typingIndicator.frame = contentBubble.bounds.inset(by: insets)
+ }
+
+ // MARK: - Animation API
+
+ open func startAnimating() {
+ defer { isAnimating = true }
+ guard !isAnimating else { return }
+ typingIndicator.startAnimating()
+ if isPulseEnabled {
+ contentBubble.layer.add(contentPulseAnimationLayer, forKey: AnimationKeys.pulse)
+ [cornerBubble, tinyBubble].forEach { $0.layer.add(circlePulseAnimationLayer, forKey: AnimationKeys.pulse) }
}
-
+ }
+
+ open func stopAnimating() {
+ defer { isAnimating = false }
+ guard isAnimating else { return }
+ typingIndicator.stopAnimating()
+ [contentBubble, cornerBubble, tinyBubble].forEach { $0.layer.removeAnimation(forKey: AnimationKeys.pulse) }
+ }
+
+ // MARK: Public
+
+ public private(set) var isAnimating = false
+
+ // MARK: - Subviews
+
+ /// The indicator used to display the typing animation.
+ public let typingIndicator = TypingIndicator()
+
+ public let contentBubble = UIView()
+
+ public let cornerBubble = BubbleCircle()
+
+ public let tinyBubble = BubbleCircle()
+
+ // MARK: Private
+
+ private enum AnimationKeys {
+ static let pulse = "typingBubble.pulse"
+ }
}
diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift
index a287781ce..355600987 100644
--- a/Sources/Views/TypingIndicator.swift
+++ b/Sources/Views/TypingIndicator.swift
@@ -1,165 +1,164 @@
-/*
- MIT License
-
- Copyright (c) 2017-2019 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2019 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import UIKit
/// A `UIView` subclass that holds 3 dots which can be animated
open class TypingIndicator: UIView {
-
- // MARK: - Properties
-
- /// The offset that each dot will transform by during the bounce animation
- public var bounceOffset: CGFloat = 2.5
-
- /// A convenience accessor for the `backgroundColor` of each dot
- open var dotColor: UIColor = UIColor.typingIndicatorDot {
- didSet {
- dots.forEach { $0.backgroundColor = dotColor }
- }
- }
-
- /// A flag that determines if the bounce animation is added in `startAnimating()`
- public var isBounceEnabled: Bool = false
-
- /// A flag that determines if the opacity animation is added in `startAnimating()`
- public var isFadeEnabled: Bool = true
-
- /// A flag indicating the animation state
- public private(set) var isAnimating: Bool = false
-
- /// Keys for each animation layer
- private struct AnimationKeys {
- static let offset = "typingIndicator.offset"
- static let bounce = "typingIndicator.bounce"
- static let opacity = "typingIndicator.opacity"
- }
-
- /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE to move the dot to the correct
- /// initial offset
- open var initialOffsetAnimationLayer: CABasicAnimation {
- let animation = CABasicAnimation(keyPath: "transform.translation.y")
- animation.byValue = -bounceOffset
- animation.duration = 0.5
- animation.isRemovedOnCompletion = true
- return animation
- }
-
- /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE
- open var bounceAnimationLayer: CABasicAnimation {
- let animation = CABasicAnimation(keyPath: "transform.translation.y")
- animation.toValue = -bounceOffset
- animation.fromValue = bounceOffset
- animation.duration = 0.5
- animation.repeatCount = .infinity
- animation.autoreverses = true
- return animation
- }
-
- /// The `CABasicAnimation` applied when `isFadeEnabled` is TRUE
- open var opacityAnimationLayer: CABasicAnimation {
- let animation = CABasicAnimation(keyPath: "opacity")
- animation.fromValue = 1
- animation.toValue = 0.5
- animation.duration = 0.5
- animation.repeatCount = .infinity
- animation.autoreverses = true
- return animation
- }
-
- // MARK: - Subviews
-
- public let stackView = UIStackView()
-
- public let dots: [BubbleCircle] = {
- return [BubbleCircle(), BubbleCircle(), BubbleCircle()]
- }()
-
- // MARK: - Initialization
-
- public override init(frame: CGRect) {
- super.init(frame: frame)
- setupView()
- }
-
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- setupView()
- }
-
- /// Sets up the view
- private func setupView() {
- dots.forEach {
- $0.backgroundColor = dotColor
- $0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
- stackView.addArrangedSubview($0)
- }
- stackView.axis = .horizontal
- stackView.alignment = .center
- stackView.distribution = .fillEqually
- addSubview(stackView)
+ // MARK: Lifecycle
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupView()
+ }
+
+ public required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ setupView()
+ }
+
+ // MARK: Open
+
+ /// A convenience accessor for the `backgroundColor` of each dot
+ open var dotColor = UIColor.typingIndicatorDot {
+ didSet {
+ dots.forEach { $0.backgroundColor = dotColor }
}
-
- // MARK: - Layout
-
- open override func layoutSubviews() {
- super.layoutSubviews()
- stackView.frame = bounds
- stackView.spacing = bounds.width > 0 ? 5 : 0
+ }
+
+ /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE to move the dot to the correct
+ /// initial offset
+ open var initialOffsetAnimationLayer: CABasicAnimation {
+ let animation = CABasicAnimation(keyPath: "transform.translation.y")
+ animation.byValue = -bounceOffset
+ animation.duration = 0.5
+ animation.isRemovedOnCompletion = true
+ return animation
+ }
+
+ /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE
+ open var bounceAnimationLayer: CABasicAnimation {
+ let animation = CABasicAnimation(keyPath: "transform.translation.y")
+ animation.toValue = -bounceOffset
+ animation.fromValue = bounceOffset
+ animation.duration = 0.5
+ animation.repeatCount = .infinity
+ animation.autoreverses = true
+ return animation
+ }
+
+ /// The `CABasicAnimation` applied when `isFadeEnabled` is TRUE
+ open var opacityAnimationLayer: CABasicAnimation {
+ let animation = CABasicAnimation(keyPath: "opacity")
+ animation.fromValue = 1
+ animation.toValue = 0.5
+ animation.duration = 0.5
+ animation.repeatCount = .infinity
+ animation.autoreverses = true
+ return animation
+ }
+
+ open override func layoutSubviews() {
+ super.layoutSubviews()
+ stackView.frame = bounds
+ stackView.spacing = bounds.width > 0 ? 5 : 0
+ }
+
+ // MARK: - Animation API
+
+ /// Sets the state of the `TypingIndicator` to animating and applies animation layers
+ open func startAnimating() {
+ defer { isAnimating = true }
+ guard !isAnimating else { return }
+ var delay: TimeInterval = 0
+ for dot in dots {
+ let currentDelay = delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+ guard let self = self else { return }
+ if self.isBounceEnabled {
+ dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset)
+ let bounceLayer = self.bounceAnimationLayer
+ bounceLayer.timeOffset = currentDelay + 0.33
+ dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce)
+ }
+ if self.isFadeEnabled {
+ dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity)
+ }
+ }
+ delay += 0.33
}
-
- // MARK: - Animation API
-
- /// Sets the state of the `TypingIndicator` to animating and applies animation layers
- open func startAnimating() {
- defer { isAnimating = true }
- guard !isAnimating else { return }
- var delay: TimeInterval = 0
- for dot in dots {
- DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
- guard let `self` = self else { return }
- if self.isBounceEnabled {
- dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset)
- let bounceLayer = self.bounceAnimationLayer
- bounceLayer.timeOffset = delay + 0.33
- dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce)
- }
- if self.isFadeEnabled {
- dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity)
- }
- }
- delay += 0.33
- }
+ }
+
+ /// Sets the state of the `TypingIndicator` to not animating and removes animation layers
+ open func stopAnimating() {
+ defer { isAnimating = false }
+ guard isAnimating else { return }
+ dots.forEach {
+ $0.layer.removeAnimation(forKey: AnimationKeys.bounce)
+ $0.layer.removeAnimation(forKey: AnimationKeys.opacity)
}
-
- /// Sets the state of the `TypingIndicator` to not animating and removes animation layers
- open func stopAnimating() {
- defer { isAnimating = false }
- guard isAnimating else { return }
- dots.forEach {
- $0.layer.removeAnimation(forKey: AnimationKeys.bounce)
- $0.layer.removeAnimation(forKey: AnimationKeys.opacity)
- }
+ }
+
+ // MARK: Public
+
+ /// The offset that each dot will transform by during the bounce animation
+ public var bounceOffset: CGFloat = 2.5
+
+ /// A flag that determines if the bounce animation is added in `startAnimating()`
+ public var isBounceEnabled = false
+
+ /// A flag that determines if the opacity animation is added in `startAnimating()`
+ public var isFadeEnabled = true
+
+ /// A flag indicating the animation state
+ public private(set) var isAnimating = false
+
+ // MARK: - Subviews
+
+ public let stackView = UIStackView()
+
+ public let dots: [BubbleCircle] = {
+ [BubbleCircle(), BubbleCircle(), BubbleCircle()]
+ }()
+
+ // MARK: Private
+
+ /// Keys for each animation layer
+ private enum AnimationKeys {
+ static let offset = "typingIndicator.offset"
+ static let bounce = "typingIndicator.bounce"
+ static let opacity = "typingIndicator.opacity"
+ }
+
+ /// Sets up the view
+ private func setupView() {
+ dots.forEach {
+ $0.backgroundColor = dotColor
+ $0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
+ stackView.addArrangedSubview($0)
}
-
+ stackView.axis = .horizontal
+ stackView.alignment = .center
+ stackView.distribution = .fillEqually
+ addSubview(stackView)
+ }
}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 17f629526..616d3b476 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -1,30 +1,28 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
-import XCTest
import MessageKitTests
+import XCTest
var tests = [XCTestCaseEntry]()
tests += MessageKitTests.allTests()
diff --git a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift
index 2439a80e9..78cae636b 100644
--- a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift
+++ b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift
@@ -1,151 +1,169 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
-final class MessageLabelTests: XCTestCase {
+// MARK: - MessageLabelTests
- let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"]
- let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"]
-
- func testMentionDetection() {
- let messageLabel: MessageLabel = MessageLabel()
- let detector: DetectorType = DetectorType.mention
- let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Mention"): "MentionDetected"]
-
- let text = mentionsList.joined(separator: " #test ")
- set(text: text, and: [detector], with: attributes, to: messageLabel)
- let matches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(matches, mentionsList)
-
- let invalids = hashtagsList.joined(separator: " ")
- set(text: invalids, and: [detector], with: attributes, to: messageLabel)
- let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(invalidMatches.count, 0)
- }
+@MainActor
+final class MessageLabelTests: XCTestCase {
+ // MARK: Internal
+
+ let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"]
+ let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"]
+
+ func testMentionDetection() {
+ let messageLabel = MessageLabel()
+ let detector = DetectorType.mention
+ let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Mention"): "MentionDetected"]
+
+ let text = mentionsList.joined(separator: " #test ")
+ set(text: text, and: [detector], with: attributes, to: messageLabel)
+ let matches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(matches, mentionsList)
+
+ let invalids = hashtagsList.joined(separator: " ")
+ set(text: invalids, and: [detector], with: attributes, to: messageLabel)
+ let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(invalidMatches.count, 0)
+ }
+
+ func testHashtagDetection() {
+ let messageLabel = MessageLabel()
+ let detector = DetectorType.hashtag
+ let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Hashtag"): "HashtagDetected"]
+
+ let text = hashtagsList.joined(separator: " @test ")
+ set(text: text, and: [detector], with: attributes, to: messageLabel)
+ let matches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(matches, hashtagsList)
+
+ let invalids = mentionsList.joined(separator: " ")
+ set(text: invalids, and: [detector], with: attributes, to: messageLabel)
+ let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(invalidMatches.count, 0)
+ }
+
+ func testCustomDetection() {
+ let shouldPass = ["1234", "1", "09876"]
+ let shouldFailed = ["abcd", "a", "!!!", ";"]
+
+ let messageLabel = MessageLabel()
+ let detector = DetectorType.custom(try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive))
+ let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Custom"): "CustomDetected"]
+
+ let text = shouldPass.joined(separator: " ")
+ set(text: text, and: [detector], with: attributes, to: messageLabel)
+ let matches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(matches, shouldPass)
+
+ let invalids = shouldFailed.joined(separator: " ")
+ set(text: invalids, and: [detector], with: attributes, to: messageLabel)
+ let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
+ XCTAssertEqual(invalidMatches.count, 0)
+ }
+
+ func testCustomDetectionOverlapping() {
+ let testText = "address MNtz8Zz1cPD1CZadoc38jT5qeqeFBS6Aif can match multiple regex's"
- func testHashtagDetection() {
- let messageLabel: MessageLabel = MessageLabel()
- let detector: DetectorType = DetectorType.hashtag
- let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Hashtag"): "HashtagDetected"]
-
- let text = hashtagsList.joined(separator: " @test ")
- set(text: text, and: [detector], with: attributes, to: messageLabel)
- let matches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(matches, hashtagsList)
-
- let invalids = mentionsList.joined(separator: " ")
- set(text: invalids, and: [detector], with: attributes, to: messageLabel)
- let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(invalidMatches.count, 0)
- }
-
- func testCustomDetection() {
- let shouldPass = ["1234", "1", "09876"]
- let shouldFailed = ["abcd", "a", "!!!", ";"]
-
- let messageLabel: MessageLabel = MessageLabel()
- let detector: DetectorType = DetectorType.custom(try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive))
- let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Custom"): "CustomDetected"]
-
- let text = shouldPass.joined(separator: " ")
- set(text: text, and: [detector], with: attributes, to: messageLabel)
- let matches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(matches, shouldPass)
-
- let invalids = shouldFailed.joined(separator: " ")
- set(text: invalids, and: [detector], with: attributes, to: messageLabel)
- let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel)
- XCTAssertEqual(invalidMatches.count, 0)
- }
+ 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))]
- func testSyncBetweenAttributedAndText() {
- let messageLabel: MessageLabel = MessageLabel()
- let expectedText = "Some text"
- messageLabel.attributedText = NSAttributedString(string: expectedText)
- XCTAssertEqual(messageLabel.text, expectedText)
-
- messageLabel.attributedText = nil
- XCTAssertNil(messageLabel.text)
-
- messageLabel.text = "some"
- messageLabel.text = expectedText
- XCTAssertEqual(messageLabel.attributedText?.string, expectedText)
-
- messageLabel.attributedText = NSAttributedString(string: "Not nil")
- messageLabel.text = nil
- XCTAssertNil(messageLabel.attributedText)
- }
-
- // MARK: - Private helpers API for Detectors
-
- /**
- Takes a given `DetectorType` and extract matches from a `MessageLabel`
-
- - Parameters: detector: `DetectorType` that you want to extract
- - Parameters: label: `MessageLabel` where you want to get matches
-
- - Returns: an array of `String` that contains all matches for the given detector
- */
- private func extractCustomDetectors(for detector: DetectorType, with label: MessageLabel) -> [String] {
- guard let detection = label.rangesForDetectors[detector] else { return [] }
- return detection.compactMap ({ (range, messageChecking) -> String? in
- switch messageChecking {
- case .custom(_, let match):
- return match
- default:
- return nil
- }
- })
- }
-
- /**
- Simply set text, detectors and attriutes to a given label
-
- - Parameters: text: `String` that will be applied to the label
- - Parameters: detector: `DetectorType` that you want to apply to the label
- - Parameters: attributes: `[NSAttributedString.Key: Any]` that you want to apply to the label
- - Parameters: label: `MessageLabel` that takes the previous parameters
-
- */
- private func set(text: String, and detectors: [DetectorType], with attributes: [NSAttributedString.Key: Any], to label: MessageLabel) {
- label.mentionAttributes = attributes
- label.enabledDetectors = detectors
- label.text = text
- }
-
+ 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()
+ let expectedText = "Some text"
+ messageLabel.attributedText = NSAttributedString(string: expectedText)
+ XCTAssertEqual(messageLabel.text, expectedText)
+
+ messageLabel.attributedText = nil
+ XCTAssertNil(messageLabel.text)
+
+ messageLabel.text = "some"
+ messageLabel.text = expectedText
+ XCTAssertEqual(messageLabel.attributedText?.string, expectedText)
+
+ messageLabel.attributedText = NSAttributedString(string: "Not nil")
+ messageLabel.text = nil
+ XCTAssertNil(messageLabel.attributedText)
+ }
+
+ // MARK: Private
+
+ // MARK: - Private helpers API for Detectors
+
+ /// Takes a given `DetectorType` and extract matches from a `MessageLabel`
+ ///
+ /// - Parameters: detector: `DetectorType` that you want to extract
+ /// - Parameters: label: `MessageLabel` where you want to get matches
+ ///
+ /// - Returns: an array of `String` that contains all matches for the given detector
+ private func extractCustomDetectors(for detector: DetectorType, with label: MessageLabel) -> [String] {
+ guard let detection = label.rangesForDetectors[detector] else { return [] }
+ return detection.compactMap({ _, messageChecking -> String? in
+ switch messageChecking {
+ case .custom(_, let match):
+ return match
+ default:
+ return nil
+ }
+ })
+ }
+
+ /// Simply set text, detectors and attriutes to a given label
+ ///
+ /// - Parameters: text: `String` that will be applied to the label
+ /// - Parameters: detector: `DetectorType` that you want to apply to the label
+ /// - Parameters: attributes: `[NSAttributedString.Key: Any]` that you want to apply to the label
+ /// - Parameters: label: `MessageLabel` that takes the previous parameters
+ ///
+ private func set(
+ text: String,
+ and detectors: [DetectorType],
+ with attributes: [NSAttributedString.Key: Any],
+ to label: MessageLabel)
+ {
+ label.mentionAttributes = attributes
+ label.enabledDetectors = detectors
+ label.text = text
+ }
}
// MARK: - Helpers
-private extension MessageLabel {
-
- var textAttributes: [NSAttributedString.Key: Any] {
- let length = attributedText!.length
- var range = NSRange(location: 0, length: length)
- return attributedText!.attributes(at: 0, effectiveRange: &range)
- }
+extension MessageLabel {
+ private var textAttributes: [NSAttributedString.Key: Any] {
+ let length = attributedText!.length
+ var range = NSRange(location: 0, length: length)
+ return attributedText!.attributes(at: 0, effectiveRange: &range)
+ }
}
diff --git a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift
index 491327062..417bcea11 100644
--- a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift
+++ b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift
@@ -1,62 +1,53 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
-import XCTest
import CoreLocation
+import XCTest
@testable import MessageKit
-final class MessagesViewControllerTests: XCTestCase {
+// MARK: - MessagesViewControllerTests
- var sut: MessagesViewController!
- // swiftlint:disable weak_delegate
- private var layoutDelegate = MockLayoutDelegate()
- // swiftlint:enable weak_delegate
-
- // MARK: - Overridden Methods
+@MainActor
+final class MessagesViewControllerTests: XCTestCase {
- override func setUp() {
- super.setUp()
+ // MARK: - Private helper API
- sut = MessagesViewController()
+ private func makeSUT() -> MessagesViewController {
+ let sut = MessagesViewController()
sut.messagesCollectionView.messagesLayoutDelegate = layoutDelegate
sut.messagesCollectionView.messagesDisplayDelegate = layoutDelegate
_ = sut.view
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
sut.view.layoutIfNeeded()
- }
- override func tearDown() {
- sut = nil
-
- super.tearDown()
+ return sut
}
// MARK: - Test
func testNumberOfSectionWithoutData_isZero() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0)
@@ -64,6 +55,7 @@ final class MessagesViewControllerTests: XCTestCase {
func testNumberOfSection_isNumberOfMessages() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
messagesDataSource.messages = makeMessages(for: messagesDataSource.senders)
@@ -77,6 +69,7 @@ final class MessagesViewControllerTests: XCTestCase {
func testNumberOfItemInSection_isOne() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
messagesDataSource.messages = makeMessages(for: messagesDataSource.senders)
@@ -88,15 +81,18 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithTextData_returnsTextMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- messagesDataSource.messages.append(MockMessage(text: "Test",
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ text: "Test",
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is TextMessageCell)
@@ -104,17 +100,20 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithAttributedTextData_returnsTextMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
let attributes = [NSAttributedString.Key.foregroundColor: UIColor.outgoingMessageLabel]
let attriutedString = NSAttributedString(string: "Test", attributes: attributes)
- messagesDataSource.messages.append(MockMessage(attributedText: attriutedString,
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ attributedText: attriutedString,
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is TextMessageCell)
@@ -122,15 +121,18 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithPhotoData_returnsMediaMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- messagesDataSource.messages.append(MockMessage(image: UIImage(),
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ image: UIImage(),
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is MediaMessageCell)
@@ -138,15 +140,18 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithVideoData_returnsMediaMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- messagesDataSource.messages.append(MockMessage(thumbnail: UIImage(),
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ thumbnail: UIImage(),
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is MediaMessageCell)
@@ -154,15 +159,18 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithLocationData_returnsLocationMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- messagesDataSource.messages.append(MockMessage(location: CLLocation(latitude: 60.0, longitude: 70.0),
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ location: CLLocation(latitude: 60.0, longitude: 70.0),
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is LocationMessageCell)
@@ -170,16 +178,19 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithAudioData_returnsAudioMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- messagesDataSource.messages.append(MockMessage(audioURL: URL.init(fileURLWithPath: ""),
- duration: 4.0,
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ audioURL: URL(fileURLWithPath: ""),
+ duration: 4.0,
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is AudioMessageCell)
@@ -187,37 +198,34 @@ final class MessagesViewControllerTests: XCTestCase {
func testCellForItemWithLinkPreviewData_returnsLinkPreviewMessageCell() {
let messagesDataSource = MockMessagesDataSource()
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = messagesDataSource
- let linkItem = MockLinkItem(text: "https://link.test",
- attributedText: nil,
- url: URL(string: "https://github.com/MessageKit")!,
- title: "Link Title",
- teaser: "Link Teaser",
- thumbnailImage: UIImage())
+ let linkItem = MockLinkItem(
+ text: "https://link.test",
+ attributedText: nil,
+ url: URL(string: "https://github.com/MessageKit")!,
+ title: "Link Title",
+ teaser: "Link Teaser",
+ thumbnailImage: UIImage())
- messagesDataSource.messages.append(MockMessage(linkItem: linkItem,
- user: messagesDataSource.senders[0],
- messageId: "test_id"))
+ messagesDataSource.messages.append(MockMessage(
+ linkItem: linkItem,
+ user: messagesDataSource.senders[0],
+ messageId: "test_id"))
sut.messagesCollectionView.reloadData()
- let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView,
- cellForItemAt: IndexPath(item: 0, section: 0))
+ let cell = sut.messagesCollectionView.dataSource?.collectionView(
+ sut.messagesCollectionView,
+ cellForItemAt: IndexPath(item: 0, section: 0))
XCTAssertNotNil(cell)
XCTAssertTrue(cell is LinkPreviewMessageCell)
}
- // MARK: - Assistants
-
- private func makeMessages(for senders: [MockUser]) -> [MessageType] {
- return [MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"),
- MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2")]
- }
-
// MARK: - Setups
-
+
func testSubviewsSetup() {
let controller = MessagesViewController()
XCTAssertTrue(controller.view.subviews.contains(controller.messagesCollectionView))
@@ -229,36 +237,53 @@ final class MessagesViewControllerTests: XCTestCase {
XCTAssertTrue(controller.messagesCollectionView.delegate is MessagesViewController)
XCTAssertTrue(controller.messagesCollectionView.dataSource is MessagesViewController)
}
-
+
func testDefaultPropertyValues() {
let controller = MessagesViewController()
- XCTAssertTrue(controller.canBecomeFirstResponder)
- XCTAssertFalse(controller.shouldAutorotate)
- XCTAssertNotNil(controller.inputAccessoryView)
XCTAssertNotNil(controller.messagesCollectionView)
XCTAssertTrue(controller.messagesCollectionView.collectionViewLayout is MessagesCollectionViewFlowLayout)
-
+
controller.view.layoutIfNeeded()
XCTAssertTrue(controller.extendedLayoutIncludesOpaqueBars)
XCTAssertEqual(controller.view.backgroundColor, UIColor.collectionViewBackground)
XCTAssertEqual(controller.messagesCollectionView.keyboardDismissMode, UIScrollView.KeyboardDismissMode.interactive)
XCTAssertTrue(controller.messagesCollectionView.alwaysBounceVertical)
}
+
+ // MARK: Private
+
+ // swiftlint:disable:next weak_delegate
+ private var layoutDelegate = MockLayoutDelegate()
+
+ // MARK: - Assistants
+
+ private func makeMessages(for senders: [MockUser]) -> [MessageType] {
+ [
+ MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"),
+ MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2"),
+ ]
+ }
}
-private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate {
+// MARK: - MockLayoutDelegate
+private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate {
// MARK: - LocationMessageLayoutDelegate
- func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 0.0
+ func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
+ 0.0
}
- func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 10.0
+ func heightForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
+ 10.0
}
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
- return LocationMessageSnapshotOptions()
+
+ func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions()
}
}
diff --git a/Tests/MessageKitTests/MessageKitTests.swift b/Tests/MessageKitTests/MessageKitTests.swift
index 7f2624469..1b5169b52 100644
--- a/Tests/MessageKitTests/MessageKitTests.swift
+++ b/Tests/MessageKitTests/MessageKitTests.swift
@@ -1,33 +1,31 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class MessageKitTests: XCTestCase {
- static var allTests = [
- ""
+ static let allTests = [
+ "",
]
}
diff --git a/Tests/MessageKitTests/Mocks/MockMessage.swift b/Tests/MessageKitTests/Mocks/MockMessage.swift
index 113166996..efca37dfa 100644
--- a/Tests/MessageKitTests/Mocks/MockMessage.swift
+++ b/Tests/MessageKitTests/Mocks/MockMessage.swift
@@ -1,133 +1,139 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+import AVFoundation
+import CoreLocation
import Foundation
import UIKit
-import CoreLocation
-import AVFoundation
@testable import MessageKit
-struct MockLocationItem: LocationItem {
+// MARK: - MockLocationItem
- var location: CLLocation
- var size: CGSize
-
- init(location: CLLocation) {
- self.location = location
- self.size = CGSize(width: 240, height: 240)
- }
+struct MockLocationItem: LocationItem {
+ var location: CLLocation
+ var size: CGSize
+ init(location: CLLocation) {
+ self.location = location
+ size = CGSize(width: 240, height: 240)
+ }
}
-struct MockMediaItem: MediaItem {
-
- var url: URL?
- var image: UIImage?
- var placeholderImage: UIImage
- var size: CGSize
-
- init(image: UIImage) {
- self.image = image
- self.size = CGSize(width: 240, height: 240)
- self.placeholderImage = UIImage()
- }
+// MARK: - MockMediaItem
+struct MockMediaItem: MediaItem {
+ var url: URL?
+ var image: UIImage?
+ var placeholderImage: UIImage
+ var size: CGSize
+
+ init(image: UIImage) {
+ self.image = image
+ size = CGSize(width: 240, height: 240)
+ placeholderImage = UIImage()
+ }
}
-private struct MockAudiotem: AudioItem {
-
- var url: URL
- var size: CGSize
- var duration: Float
-
- init(url: URL, duration: Float) {
- self.url = url
- self.size = CGSize(width: 160, height: 35)
- self.duration = duration
- }
+// MARK: - MockAudiotem
+private struct MockAudiotem: AudioItem {
+ var url: URL
+ var size: CGSize
+ var duration: Float
+
+ init(url: URL, duration: Float) {
+ self.url = url
+ size = CGSize(width: 160, height: 35)
+ self.duration = duration
+ }
}
+// MARK: - MockLinkItem
+
struct MockLinkItem: LinkItem {
- let text: String?
- let attributedText: NSAttributedString?
- let url: URL
- let title: String?
- let teaser: String
- let thumbnailImage: UIImage
+ let text: String?
+ let attributedText: NSAttributedString?
+ let url: URL
+ let title: String?
+ let teaser: String
+ let thumbnailImage: UIImage
}
-struct MockMessage: MessageType {
+// MARK: - MockMessage
- var messageId: String
- var sender: SenderType {
- return user
- }
- var sentDate: Date
- var kind: MessageKind
- var user: MockUser
-
- private init(kind: MessageKind, user: MockUser, messageId: String) {
- self.kind = kind
- self.user = user
- self.messageId = messageId
- self.sentDate = Date()
- }
-
- init(text: String, user: MockUser, messageId: String) {
- self.init(kind: .text(text), user: user, messageId: messageId)
- }
-
- init(attributedText: NSAttributedString, user: MockUser, messageId: String) {
- self.init(kind: .attributedText(attributedText), user: user, messageId: messageId)
- }
-
- init(image: UIImage, user: MockUser, messageId: String) {
- let mediaItem = MockMediaItem(image: image)
- self.init(kind: .photo(mediaItem), user: user, messageId: messageId)
- }
-
- init(thumbnail: UIImage, user: MockUser, messageId: String) {
- let mediaItem = MockMediaItem(image: thumbnail)
- self.init(kind: .video(mediaItem), user: user, messageId: messageId)
- }
-
- init(location: CLLocation, user: MockUser, messageId: String) {
- let locationItem = MockLocationItem(location: location)
- self.init(kind: .location(locationItem), user: user, messageId: messageId)
- }
-
- init(emoji: String, user: MockUser, messageId: String) {
- self.init(kind: .emoji(emoji), user: user, messageId: messageId)
- }
-
- init(audioURL: URL, duration: Float, user: MockUser, messageId: String) {
- let audioItem = MockAudiotem(url: audioURL, duration: duration)
- self.init(kind: .audio(audioItem), user: user, messageId: messageId)
- }
-
- init(linkItem: LinkItem, user: MockUser, messageId: String) {
- self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId)
- }
+struct MockMessage: MessageType {
+ // MARK: Lifecycle
+
+ private init(kind: MessageKind, user: MockUser, messageId: String) {
+ self.kind = kind
+ self.user = user
+ self.messageId = messageId
+ sentDate = Date()
+ }
+
+ init(text: String, user: MockUser, messageId: String) {
+ self.init(kind: .text(text), user: user, messageId: messageId)
+ }
+
+ init(attributedText: NSAttributedString, user: MockUser, messageId: String) {
+ self.init(kind: .attributedText(attributedText), user: user, messageId: messageId)
+ }
+
+ init(image: UIImage, user: MockUser, messageId: String) {
+ let mediaItem = MockMediaItem(image: image)
+ self.init(kind: .photo(mediaItem), user: user, messageId: messageId)
+ }
+
+ init(thumbnail: UIImage, user: MockUser, messageId: String) {
+ let mediaItem = MockMediaItem(image: thumbnail)
+ self.init(kind: .video(mediaItem), user: user, messageId: messageId)
+ }
+
+ init(location: CLLocation, user: MockUser, messageId: String) {
+ let locationItem = MockLocationItem(location: location)
+ self.init(kind: .location(locationItem), user: user, messageId: messageId)
+ }
+
+ init(emoji: String, user: MockUser, messageId: String) {
+ self.init(kind: .emoji(emoji), user: user, messageId: messageId)
+ }
+
+ init(audioURL: URL, duration: Float, user: MockUser, messageId: String) {
+ let audioItem = MockAudiotem(url: audioURL, duration: duration)
+ self.init(kind: .audio(audioItem), user: user, messageId: messageId)
+ }
+
+ init(linkItem: LinkItem, user: MockUser, messageId: String) {
+ self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId)
+ }
+
+ // MARK: Internal
+
+ var messageId: String
+ var sentDate: Date
+ var kind: MessageKind
+ var user: MockUser
+
+ var sender: SenderType {
+ user
+ }
}
diff --git a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift
index 42ecdb9e4..dc9da0138 100644
--- a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift
+++ b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift
@@ -1,52 +1,49 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
@testable import MessageKit
+@MainActor
class MockMessagesDataSource: MessagesDataSource {
-
var messages: [MessageType] = []
let senders: [MockUser] = [
MockUser(senderId: "sender_1", displayName: "Sender 1"),
- MockUser(senderId: "sender_2", displayName: "Sender 2")
+ MockUser(senderId: "sender_2", displayName: "Sender 2"),
]
var currentUser: MockUser {
- return senders[0]
+ senders[0]
}
- func currentSender() -> SenderType {
- return currentUser
+ var currentSender: SenderType {
+ currentUser
}
- func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
- return messages.count
+ func numberOfSections(in _: MessagesCollectionView) -> Int {
+ messages.count
}
- func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
- return messages[indexPath.section]
+ func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
+ messages[indexPath.section]
}
-
}
diff --git a/Tests/MessageKitTests/Mocks/MockUser.swift b/Tests/MessageKitTests/Mocks/MockUser.swift
index 9c048f0a6..a91b3f8c8 100644
--- a/Tests/MessageKitTests/Mocks/MockUser.swift
+++ b/Tests/MessageKitTests/Mocks/MockUser.swift
@@ -1,31 +1,29 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
@testable import MessageKit
struct MockUser: SenderType {
- var senderId: String
- var displayName: String
+ var senderId: String
+ var displayName: String
}
diff --git a/Tests/MessageKitTests/Model Tests/AvatarTests.swift b/Tests/MessageKitTests/Model Tests/AvatarTests.swift
index a3ed9a6f0..f87804af6 100644
--- a/Tests/MessageKitTests/Model Tests/AvatarTests.swift
+++ b/Tests/MessageKitTests/Model Tests/AvatarTests.swift
@@ -1,35 +1,33 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class AvatarTests: XCTestCase {
- func testAvatarDefaultPropertyValues() {
- let avatar = Avatar()
- XCTAssertNil(avatar.image)
- XCTAssertEqual(avatar.initials, "?")
- }
+ func testAvatarDefaultPropertyValues() {
+ let avatar = Avatar()
+ XCTAssertNil(avatar.image)
+ XCTAssertEqual(avatar.initials, "?")
+ }
}
diff --git a/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift b/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift
index 81ac4c47c..9fa84b1c6 100644
--- a/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift
+++ b/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift
@@ -1,47 +1,45 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class DetectorTypeTests: XCTestCase {
- func testMappingToNSTextCheckingResultCheckingType() {
- /// case .address should equal .address
- let address = DetectorType.address.textCheckingType
- XCTAssertEqual(address, NSTextCheckingResult.CheckingType.address)
- /// case .url should equal to .link
- let url = DetectorType.url.textCheckingType
- XCTAssertEqual(url, NSTextCheckingResult.CheckingType.link)
- /// case .date should equal to .date
- let date = DetectorType.date.textCheckingType
- XCTAssertEqual(date, NSTextCheckingResult.CheckingType.date)
- /// case .phoneNumber should equal to .phoneNumber
- let phoneNumber = DetectorType.phoneNumber.textCheckingType
- XCTAssertEqual(phoneNumber, NSTextCheckingResult.CheckingType.phoneNumber)
- /// case .transitInformation should equal to .transitInformation
- let transitInformation = DetectorType.transitInformation.textCheckingType
- XCTAssertEqual(transitInformation, NSTextCheckingResult.CheckingType.transitInformation)
- }
+ func testMappingToNSTextCheckingResultCheckingType() {
+ /// case .address should equal .address
+ let address = DetectorType.address.textCheckingType
+ XCTAssertEqual(address, NSTextCheckingResult.CheckingType.address)
+ /// case .url should equal to .link
+ let url = DetectorType.url.textCheckingType
+ XCTAssertEqual(url, NSTextCheckingResult.CheckingType.link)
+ /// case .date should equal to .date
+ let date = DetectorType.date.textCheckingType
+ XCTAssertEqual(date, NSTextCheckingResult.CheckingType.date)
+ /// case .phoneNumber should equal to .phoneNumber
+ let phoneNumber = DetectorType.phoneNumber.textCheckingType
+ XCTAssertEqual(phoneNumber, NSTextCheckingResult.CheckingType.phoneNumber)
+ /// case .transitInformation should equal to .transitInformation
+ let transitInformation = DetectorType.transitInformation.textCheckingType
+ XCTAssertEqual(transitInformation, NSTextCheckingResult.CheckingType.transitInformation)
+ }
}
diff --git a/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift b/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift
index 5f7ca643e..154a8c144 100644
--- a/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift
+++ b/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift
@@ -1,114 +1,116 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class MessageKitDateFormatterTests: XCTestCase {
-
- var formatter: DateFormatter!
- let attributes = [NSAttributedString.Key.backgroundColor: "red"]
- override func setUp() {
- super.setUp()
-
- formatter = DateFormatter()
- }
-
- override func tearDown() {
- formatter = nil
- super.tearDown()
- }
-
- func testConfigureDateFormatterToday() {
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
-
- XCTAssertEqual(MessageKitDateFormatter.shared.string(from: Date()), formatter.string(from: Date()))
- }
-
- func testConfigureDateFormatterTodayAttributed() {
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
-
- XCTAssertEqual(MessageKitDateFormatter.shared.attributedString(from: Date(), with: attributes), NSAttributedString(string: formatter.string(from: Date()), attributes: attributes))
- }
-
- func testConfigureDateFormatterYesterday() {
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
-
- let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])!
- XCTAssertEqual(MessageKitDateFormatter.shared.string(from: yesterday), formatter.string(from: yesterday))
+ var formatter: DateFormatter!
+ let attributes = [NSAttributedString.Key.backgroundColor: "red"]
+
+ override func setUp() {
+ super.setUp()
+
+ formatter = DateFormatter()
+ }
+
+ override func tearDown() {
+ formatter = nil
+ super.tearDown()
+ }
+
+ func testConfigureDateFormatterToday() {
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+
+ XCTAssertEqual(MessageKitDateFormatter.shared.string(from: Date()), formatter.string(from: Date()))
+ }
+
+ func testConfigureDateFormatterTodayAttributed() {
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+
+ XCTAssertEqual(
+ MessageKitDateFormatter.shared.attributedString(from: Date(), with: attributes),
+ NSAttributedString(string: formatter.string(from: Date()), attributes: attributes))
+ }
+
+ func testConfigureDateFormatterYesterday() {
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+
+ let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])!
+ XCTAssertEqual(MessageKitDateFormatter.shared.string(from: yesterday), formatter.string(from: yesterday))
+ }
+
+ func testConfigureDateFormatterYesterdayAttributed() {
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+
+ let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])!
+ XCTAssertEqual(
+ MessageKitDateFormatter.shared.attributedString(
+ from: yesterday,
+ with: attributes),
+ NSAttributedString(string: formatter.string(from: yesterday), attributes: attributes))
+ }
+
+ func testConfigureDateFormatterWeekAndYear() {
+ // First day of current week
+ var startOfWeek = Calendar.current
+ .date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
+ // Check if today or yesterday was the first day of the week, because it will be different format then
+ if Calendar.current.isDateInToday(startOfWeek) || Calendar.current.isDateInYesterday(startOfWeek) {
+ formatter.doesRelativeDateFormatting = true
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+
+ XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
+ } else {
+ formatter.dateFormat = "EEEE h:mm a"
+ XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
}
- func testConfigureDateFormatterYesterdayAttributed() {
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
-
- let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])!
- XCTAssertEqual(MessageKitDateFormatter.shared.attributedString(from: yesterday,
- with: attributes),
- NSAttributedString(string: formatter.string(from: yesterday), attributes: attributes))
- }
+ /// Day of last week
+ startOfWeek = (Calendar.current as NSCalendar).date(byAdding: .day, value: -2, to: startOfWeek, options: [])!
- func testConfigureDateFormatterWeekAndYear() {
- //First day of current week
- var startOfWeek = Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
- //Check if today or yesterday was the first day of the week, because it will be different format then
- if Calendar.current.isDateInToday(startOfWeek) || Calendar.current.isDateInYesterday(startOfWeek) {
- formatter.doesRelativeDateFormatting = true
- formatter.dateStyle = .short
- formatter.timeStyle = .short
-
- XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
- } else {
- formatter.dateFormat = "EEEE h:mm a"
- XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
- }
-
- ///Day of last week
- startOfWeek = (Calendar.current as NSCalendar).date(byAdding: .day, value: -2, to: startOfWeek, options: [])!
-
- if Calendar.current.isDate(startOfWeek, equalTo: Date(), toGranularity: .year) {
- formatter.dateFormat = "E, d MMM, h:mm a"
- } else {
- formatter.dateFormat = "MMM d, yyyy, h:mm a"
- }
-
- XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
+ if Calendar.current.isDate(startOfWeek, equalTo: Date(), toGranularity: .year) {
+ formatter.dateFormat = "E, d MMM, h:mm a"
+ } else {
+ formatter.dateFormat = "MMM d, yyyy, h:mm a"
}
- func testConfigureDateFormatterForMoreThanYear() {
- formatter.dateFormat = "MMM d, yyyy, h:mm a"
- let lastYear = (Calendar.current as NSCalendar).date(byAdding: .year, value: -1, to: Date(), options: [])!
+ XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek))
+ }
- XCTAssertEqual(formatter.string(from: lastYear), MessageKitDateFormatter.shared.string(from: lastYear))
- }
+ func testConfigureDateFormatterForMoreThanYear() {
+ formatter.dateFormat = "MMM d, yyyy, h:mm a"
+ let lastYear = (Calendar.current as NSCalendar).date(byAdding: .year, value: -1, to: Date(), options: [])!
+ XCTAssertEqual(formatter.string(from: lastYear), MessageKitDateFormatter.shared.string(from: lastYear))
+ }
}
diff --git a/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift b/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift
index ccd44064a..dc12297a2 100644
--- a/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift
+++ b/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift
@@ -1,157 +1,159 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class MessageStyleTests: XCTestCase {
- private let assetBundle = Bundle.messageKitAssetBundle
-
- func testTailCornerImageOrientation() {
- XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImage.Orientation.up)
- XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImage.Orientation.upMirrored)
- XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImage.Orientation.down)
- XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImage.Orientation.downMirrored)
- }
-
- func testTailStyleImageNameSuffix() {
- XCTAssertEqual(MessageStyle.TailStyle.curved.imageNameSuffix, "_tail_v2")
- XCTAssertEqual(MessageStyle.TailStyle.pointedEdge.imageNameSuffix, "_tail_v1")
- }
-
- func testImageNil() {
- XCTAssertNil(MessageStyle.none.image)
- }
-
- func testImageBubble() {
- let originalData = (MessageStyle.bubble.image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_full", in: assetBundle, compatibleWith: nil)
- let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testImageBubbleOutline() {
- let originalData = (MessageStyle.bubbleOutline(.black).image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_outlined", in: assetBundle, compatibleWith: nil)
- let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testImageBubbleTailCurved() {
- var originalData = (MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_full_tail_v2", in: assetBundle, compatibleWith: nil)
- var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testImageBubbleTailPointedEdge() {
- var originalData = (MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_full_tail_v1", in: assetBundle, compatibleWith: nil)
- var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testImageBubbleTailOutlineCurved() {
- var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_outlined_tail_v2", in: assetBundle, compatibleWith: nil)
- var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testImageBubbleTailOutlinePointedEdge() {
- var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()).pngData()
- let testImage = UIImage(named: "bubble_outlined_tail_v1", in: assetBundle, compatibleWith: nil)
- var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
-
- originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()).pngData()
- testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
- XCTAssertEqual(originalData, testData)
- }
-
- func testCachesIdenticalOutlineImagesFromCache() {
- let image1 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()
- let image2 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()
- XCTAssertEqual(image1, image2)
- // After clearing cache a new image instance will be loaded from disk
- MessageStyle.bubbleImageCache.removeAllObjects()
- XCTAssertFalse(image1 === MessageStyle.bubbleTail(.topLeft, .pointedEdge).image)
- }
-
- private func stretch(_ image: UIImage) -> UIImage {
- let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
- let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
- return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
- }
-
- private func transform(image: UIImage, corner: MessageStyle.TailCorner) -> UIImage? {
- guard let cgImage = image.cgImage else { return image }
- return UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation)
- }
+ // MARK: Internal
+
+ func testTailCornerImageOrientation() {
+ XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImage.Orientation.up)
+ XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImage.Orientation.upMirrored)
+ XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImage.Orientation.down)
+ XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImage.Orientation.downMirrored)
+ }
+
+ func testTailStyleImageNameSuffix() {
+ XCTAssertEqual(MessageStyle.TailStyle.curved.imageNameSuffix, "_tail_v2")
+ XCTAssertEqual(MessageStyle.TailStyle.pointedEdge.imageNameSuffix, "_tail_v1")
+ }
+
+ func testImageNil() {
+ XCTAssertNil(MessageStyle.none.image)
+ }
+
+ func testImageBubble() {
+ let originalData = (MessageStyle.bubble.image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_full", in: assetBundle, compatibleWith: nil)
+ let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testImageBubbleOutline() {
+ let originalData = (MessageStyle.bubbleOutline(.black).image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_outlined", in: assetBundle, compatibleWith: nil)
+ let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testImageBubbleTailCurved() {
+ var originalData = (MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_full_tail_v2", in: assetBundle, compatibleWith: nil)
+ var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testImageBubbleTailPointedEdge() {
+ var originalData = (MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_full_tail_v1", in: assetBundle, compatibleWith: nil)
+ var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testImageBubbleTailOutlineCurved() {
+ var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_outlined_tail_v2", in: assetBundle, compatibleWith: nil)
+ var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testImageBubbleTailOutlinePointedEdge() {
+ var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()).pngData()
+ let testImage = UIImage(named: "bubble_outlined_tail_v1", in: assetBundle, compatibleWith: nil)
+ var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+
+ originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()).pngData()
+ testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData()
+ XCTAssertEqual(originalData, testData)
+ }
+
+ func testCachesIdenticalOutlineImagesFromCache() {
+ let image1 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()
+ let image2 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()
+ XCTAssertEqual(image1, image2)
+ // After clearing cache a new image instance will be loaded from disk
+ MessageStyle.bubbleImageCache.removeAllObjects()
+ XCTAssertFalse(image1 === MessageStyle.bubbleTail(.topLeft, .pointedEdge).image)
+ }
+
+ // MARK: Private
+
+ private let assetBundle = Bundle.messageKitAssetBundle
+
+ private func stretch(_ image: UIImage) -> UIImage {
+ let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2)
+ let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x)
+ return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
+ }
+
+ private func transform(image: UIImage, corner: MessageStyle.TailCorner) -> UIImage? {
+ guard let cgImage = image.cgImage else { return image }
+ return UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation)
+ }
}
diff --git a/Tests/MessageKitTests/Model Tests/SenderTests.swift b/Tests/MessageKitTests/Model Tests/SenderTests.swift
index f2ddb6d8a..5bb75de24 100644
--- a/Tests/MessageKitTests/Model Tests/SenderTests.swift
+++ b/Tests/MessageKitTests/Model Tests/SenderTests.swift
@@ -1,40 +1,39 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
final class SenderTests: XCTestCase {
- func testEqualityBetweenTwoSenders() {
- let sender1 = MockUser(senderId: "1", displayName: "Steven")
- let sender2 = MockUser(senderId: "1", displayName: "Nathan")
- XCTAssertEqual(sender1.senderId, sender2.senderId)
- }
- func testNotEqualityBetweenTwoSenders() {
- let sender1 = MockUser(senderId: "1", displayName: "Steven")
- let sender2 = MockUser(senderId: "2", displayName: "Nathan")
- XCTAssertNotEqual(sender1.senderId, sender2.senderId)
- }
+ func testEqualityBetweenTwoSenders() {
+ let sender1 = MockUser(senderId: "1", displayName: "Steven")
+ let sender2 = MockUser(senderId: "1", displayName: "Nathan")
+ XCTAssertEqual(sender1.senderId, sender2.senderId)
+ }
+
+ func testNotEqualityBetweenTwoSenders() {
+ let sender1 = MockUser(senderId: "1", displayName: "Steven")
+ let sender2 = MockUser(senderId: "2", displayName: "Nathan")
+ XCTAssertNotEqual(sender1.senderId, sender2.senderId)
+ }
}
diff --git a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift
index 9eec24fb0..2f51d61b9 100644
--- a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift
+++ b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift
@@ -1,188 +1,212 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
-final class MessagesDisplayDelegateTests: XCTestCase {
-
- private var sut: MockMessagesViewController!
+// MARK: - MessagesDisplayDelegateTests
- override func setUp() {
- super.setUp()
+@MainActor
+final class MessagesDisplayDelegateTests: XCTestCase {
+ // MARK: - Private helper API
- sut = MockMessagesViewController()
+ private func makeSUT() -> MockMessagesViewController {
+ let sut = MockMessagesViewController()
_ = sut.view
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
+ sut.viewDidLoad()
sut.view.layoutIfNeeded()
- }
-
- override func tearDown() {
- sut = nil
- super.tearDown()
+ return sut
}
+ // MARK: Internal
+
func testBackGroundColorDefaultState() {
- XCTAssertEqual(sut.backgroundColor(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView),
- UIColor.outgoingMessageBackground)
- XCTAssertNotEqual(sut.backgroundColor(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView),
- UIColor.incomingMessageBackground)
- XCTAssertEqual(sut.backgroundColor(for: sut.dataProvider.messages[1],
- at: IndexPath(item: 1, section: 0),
- in: sut.messagesCollectionView),
- UIColor.incomingMessageBackground)
- XCTAssertNotEqual(sut.backgroundColor(for: sut.dataProvider.messages[1],
- at: IndexPath(item: 1, section: 0),
- in: sut.messagesCollectionView),
- UIColor.outgoingMessageBackground)
+ let sut = makeSUT()
+ XCTAssertEqual(
+ sut.backgroundColor(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView),
+ UIColor.outgoingMessageBackground)
+ XCTAssertNotEqual(
+ sut.backgroundColor(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView),
+ UIColor.incomingMessageBackground)
+ XCTAssertEqual(
+ sut.backgroundColor(
+ for: sut.dataProvider.messages[1],
+ at: IndexPath(item: 1, section: 0),
+ in: sut.messagesCollectionView),
+ UIColor.incomingMessageBackground)
+ XCTAssertNotEqual(
+ sut.backgroundColor(
+ for: sut.dataProvider.messages[1],
+ at: IndexPath(item: 1, section: 0),
+ in: sut.messagesCollectionView),
+ UIColor.outgoingMessageBackground)
}
func testBackgroundColorWithoutDataSource_returnsWhiteForDefault() {
+ let sut = makeSUT()
sut.messagesCollectionView.messagesDataSource = nil
- let backgroundColor = sut.backgroundColor(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let backgroundColor = sut.backgroundColor(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
XCTAssertEqual(backgroundColor, UIColor.white)
}
func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() {
- sut.dataProvider.messages.append(MockMessage(emoji: "🤔",
- user: sut.dataProvider.currentUser,
- messageId: "003"))
- let backgroundColor = sut.backgroundColor(for: sut.dataProvider.messages[2],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let sut = makeSUT()
+ sut.dataProvider.messages.append(MockMessage(
+ emoji: "🤔",
+ user: sut.dataProvider.currentUser,
+ messageId: "003"))
+ let backgroundColor = sut.backgroundColor(
+ for: sut.dataProvider.messages[2],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
XCTAssertEqual(backgroundColor, .clear)
}
func testCellTopLabelDefaultState() {
- XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0)))
+ let sut = makeSUT()
+ XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0)))
}
func testMessageBottomLabelDefaultState() {
- XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0)))
+ let sut = makeSUT()
+ XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0)))
}
func testMessageStyle_returnsBubleTypeForDefault() {
- let type = sut.messageStyle(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let sut = makeSUT()
+ let type = sut.messageStyle(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
let result: Bool
switch type {
- case .bubble:
- result = true
- default:
- result = false
+ case .bubble:
+ result = true
+ default:
+ result = false
}
XCTAssertTrue(result)
}
func testMessageHeaderView_isNotNil() {
+ let sut = makeSUT()
let indexPath = IndexPath(item: 0, section: 1)
+ XCTAssert(sut.dataProvider != nil)
let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView)
XCTAssertNotNil(headerView)
}
-
}
-class TextMessageDisplayDelegateTests: XCTestCase {
-
- private var sut: MockMessagesViewController!
+// MARK: - TextMessageDisplayDelegateTests
- override func setUp() {
- super.setUp()
+@MainActor
+class TextMessageDisplayDelegateTests: XCTestCase {
+ // MARK: - Private helper API
- sut = MockMessagesViewController()
+ private func makeSUT() -> MockMessagesViewController {
+ let sut = MockMessagesViewController()
_ = sut.view
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
- }
-
- override func tearDown() {
- sut = nil
-
- super.tearDown()
+ return sut
}
func testTextColorFromCurrentSender_returnsWhiteForDefault() {
- let textColor = sut.textColor(for: sut.dataProvider.messages[0],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let sut = makeSUT()
+ let textColor = sut.textColor(
+ for: sut.dataProvider.messages[0],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
XCTAssertEqual(textColor, UIColor.outgoingMessageLabel)
}
func testTextColorFromYou_returnsDarkTextForDefault() {
- let textColor = sut.textColor(for: sut.dataProvider.messages[1],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let sut = makeSUT()
+ let textColor = sut.textColor(
+ for: sut.dataProvider.messages[1],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
XCTAssertEqual(textColor, UIColor.incomingMessageLabel)
}
func testTextColorWithoutDataSource_returnsDarkTextForDefault() {
+ let sut = makeSUT()
let dataSource = sut.makeDataSource()
sut.messagesCollectionView.messagesDataSource = dataSource
- let textColor = sut.textColor(for: sut.dataProvider.messages[1],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let textColor = sut.textColor(
+ for: sut.dataProvider.messages[1],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
XCTAssertEqual(textColor, UIColor.incomingMessageLabel)
}
func testEnableDetectors_returnsEmptyForDefault() {
- let detectors = sut.enabledDetectors(for: sut.dataProvider.messages[1],
- at: IndexPath(item: 0, section: 0),
- in: sut.messagesCollectionView)
+ let sut = makeSUT()
+ let detectors = sut.enabledDetectors(
+ for: sut.dataProvider.messages[1],
+ at: IndexPath(item: 0, section: 0),
+ in: sut.messagesCollectionView)
let expectedDetectors: [DetectorType] = []
XCTAssertEqual(detectors, expectedDetectors)
}
-
}
-private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate {
+// MARK: - MockMessagesViewController
- func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
- return 200
- }
+@MainActor
+private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate {
+ // MARK: Internal
var dataProvider: MockMessagesDataSource!
+ func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat {
+ 200
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
@@ -190,22 +214,31 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla
messagesCollectionView.messagesDisplayDelegate = self
messagesCollectionView.messagesDataSource = dataProvider
messagesCollectionView.messagesLayoutDelegate = self
+ messagesCollectionView.reloadData()
+ }
+ func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions()
}
+ // MARK: Fileprivate
+
fileprivate func makeDataSource() -> MockMessagesDataSource {
let dataSource = MockMessagesDataSource()
- dataSource.messages.append(MockMessage(text: "Text 1",
- user: dataSource.senders[0],
- messageId: "001"))
- dataSource.messages.append(MockMessage(text: "Text 2",
- user: dataSource.senders[1],
- messageId: "002"))
+ dataSource.messages.append(MockMessage(
+ text: "Text 1",
+ user: dataSource.senders[0],
+ messageId: "001"))
+ dataSource.messages.append(MockMessage(
+ text: "Text 2",
+ user: dataSource.senders[1],
+ messageId: "002"))
return dataSource
}
-
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
- return LocationMessageSnapshotOptions()
- }
}
diff --git a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift
index de3c070d2..846b1c01e 100644
--- a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift
+++ b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift
@@ -1,47 +1,42 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
+@MainActor
final class AvatarViewTests: XCTestCase {
+ // MARK: - Private helper API
- var avatarView: AvatarView!
-
- override func setUp() {
- super.setUp()
- avatarView = AvatarView()
+ private func makeAvatarView() -> AvatarView {
+ let avatarView = AvatarView()
avatarView.frame.size = CGSize(width: 30, height: 30)
- }
-
- override func tearDown() {
- super.tearDown()
- avatarView = nil
+ return avatarView
}
func testNoParams() {
+ let avatarView = makeAvatarView()
+
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
// For certain dynamic colors, need to compare cgColor in XCTest
// https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor
@@ -49,6 +44,8 @@ final class AvatarViewTests: XCTestCase {
}
func testWithImage() {
+ let avatarView = makeAvatarView()
+
let avatar = Avatar(image: UIImage())
avatarView.set(avatar: avatar)
XCTAssertEqual(avatar.initials, "?")
@@ -57,6 +54,8 @@ final class AvatarViewTests: XCTestCase {
}
func testInitialsOnly() {
+ let avatarView = makeAvatarView()
+
let avatar = Avatar(initials: "DL")
avatarView.set(avatar: avatar)
XCTAssertEqual(avatarView.initials, avatar.initials)
@@ -66,12 +65,15 @@ final class AvatarViewTests: XCTestCase {
}
func testSetBackground() {
+ let avatarView = makeAvatarView()
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor)
avatarView.backgroundColor = UIColor.red
XCTAssertEqual(avatarView.backgroundColor!, UIColor.red)
}
func testGetImage() {
+ let avatarView = makeAvatarView()
+
let image = UIImage()
let avatar = Avatar(image: image)
avatarView.set(avatar: avatar)
@@ -79,6 +81,8 @@ final class AvatarViewTests: XCTestCase {
}
func testRoundedCorners() {
+ let avatarView = makeAvatarView()
+
let avatar = Avatar(image: UIImage())
avatarView.set(avatar: avatar)
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
diff --git a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift
index b34e47b1a..62190e3ea 100644
--- a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift
+++ b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift
@@ -1,61 +1,52 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import Foundation
import XCTest
@testable import MessageKit
-final class MessageContentCellTests: XCTestCase {
+// MARK: - MessageContentCellTests
- var cell: MessageContentCell!
+@MainActor
+final class MessageContentCellTests: XCTestCase {
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
- override func setUp() {
- super.setUp()
- cell = MessageContentCell(frame: frame)
- }
-
- override func tearDown() {
- cell = nil
- super.tearDown()
- }
-
func testInit() {
+ let cell = MessageContentCell(frame: frame)
XCTAssertEqual(cell.contentView.autoresizingMask, [.flexibleWidth, .flexibleHeight])
XCTAssert(cell.contentView.subviews.contains(cell.cellTopLabel))
XCTAssert(cell.contentView.subviews.contains(cell.messageBottomLabel))
XCTAssert(cell.contentView.subviews.contains(cell.avatarView))
XCTAssert(cell.contentView.subviews.contains(cell.messageContainerView))
-
}
func testMessageContainerViewPropertiesSetup() {
+ let cell = MessageContentCell(frame: frame)
XCTAssertTrue(cell.messageContainerView.clipsToBounds)
XCTAssertTrue(cell.messageContainerView.layer.masksToBounds)
}
func testPrepareForReuse() {
+ let cell = MessageContentCell(frame: frame)
cell.prepareForReuse()
XCTAssertNil(cell.cellTopLabel.text)
XCTAssertNil(cell.cellTopLabel.attributedText)
@@ -64,6 +55,7 @@ final class MessageContentCellTests: XCTestCase {
}
func testApplyLayoutAttributes() {
+ let cell = MessageContentCell(frame: frame)
let layoutAttributes = MessagesCollectionViewLayoutAttributes()
layoutAttributes.avatarPosition = AvatarPosition(horizontal: .cellLeading, vertical: .cellBottom)
cell.apply(layoutAttributes)
@@ -73,14 +65,17 @@ final class MessageContentCellTests: XCTestCase {
XCTAssertEqual(cell.cellTopLabel.frame.size, layoutAttributes.cellTopLabelSize)
XCTAssertEqual(cell.messageBottomLabel.frame.size, layoutAttributes.messageBottomLabelSize)
}
-
}
extension MessageContentCellTests {
private class MockMessagesDisplayDelegate: MessagesDisplayDelegate {
- func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
- return LocationMessageSnapshotOptions()
+ func snapshotOptionsForLocation(
+ message _: MessageType,
+ at _: IndexPath,
+ in _: MessagesCollectionView)
+ -> LocationMessageSnapshotOptions
+ {
+ LocationMessageSnapshotOptions()
}
}
-
}
diff --git a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift
index 27fd32e39..84e0ca902 100644
--- a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift
+++ b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift
@@ -1,50 +1,37 @@
-/*
- MIT License
-
- Copyright (c) 2017-2020 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
+// MIT License
+//
+// Copyright (c) 2017-2020 MessageKit
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
import XCTest
@testable import MessageKit
+@MainActor
class MessagesCollectionViewTests: XCTestCase {
-
- var messagesCollectionView: MessagesCollectionView!
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let layout = MessagesCollectionViewFlowLayout()
- override func setUp() {
- super.setUp()
- messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout)
- }
-
- override func tearDown() {
- messagesCollectionView = nil
- super.tearDown()
- }
-
func testInit() {
+ let messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout)
XCTAssertEqual(messagesCollectionView.frame, rect)
XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout)
XCTAssertEqual(messagesCollectionView.backgroundColor, UIColor.collectionViewBackground)
}
-
}
diff --git a/VISION.md b/VISION.md
deleted file mode 100644
index e071591fd..000000000
--- a/VISION.md
+++ /dev/null
@@ -1,25 +0,0 @@
-## MessageKit Vision
-
-### Goals
-- Provide a suitable replacement (but not absolute mirror image) of JSQMessagesViewController.
-- Provide “sensible defaults, but also customization hooks" - @jessesquires
-- Favor a Swift-first, idiomatic API.
-- Build a centralized MessageViewController project for iOS.
-- Cultivate an inclusive open source community through respectful discussion.
-
-### Scope
-#### Things we will not support:
-- Views that live outside of the MessagesViewController such as photo pickers and media players.
-- Anything that favors specific networking / caching strategies or specific libraries.
-
-Instead, MessageKit will provide you with hooks to easily handle these situations as you wish. We believe this is more flexible, maintainable, and reasonable.
-
-### Technical Considerations
-- **iOS version**:
-We will strive to support the 3 latest versions of iOS.
-
-- **Objective-C Compatibility**:
-We will not sacrifice functionality or an idiomatic Swift API to support Objective-C, but would love to improve Objective-C compatability where possible.
-
-- **Layouts**:
-We will favor programmatic layouts over `.xib`s where ever possible.