diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a6ebf6..ab2ece43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added +- **Breaking Change** Added `.hashtag`, .`mention` to detect theses pattern inside the `messageLabel`. We also add `.custom(pattern: YOUR_PATTERN)` to `DetectorType` to manage and deal with your own regular expression. +[#913](https://github.com/MessageKit/MessageKit/pull/913) by [@JulienKode](https://github.com/julienkode). + - Added support for detection and handling of `NSLink`s inside of messages. [#815](https://github.com/MessageKit/MessageKit/pull/815) by [@jnic](https://github.com/jnic) diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index bbe9c211..5c782883 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -31,32 +31,24 @@ final internal class SampleData { static let shared = SampleData() private init() {} - - enum MessageTypes: UInt32, CaseIterable { - case Text = 0 - case AttributedText = 1 - case Photo = 2 - case Video = 3 - case Audio = 4 - case Emoji = 5 - case Location = 6 - case Url = 7 - case Phone = 8 - case Custom = 9 - - static func random() -> MessageTypes { - // Update as new enumerations are added - let maxValue = Custom.rawValue - - let rand = arc4random_uniform(maxValue+1) - return MessageTypes(rawValue: rand)! - } + + enum MessageTypes: String, CaseIterable { + case Text + case AttributedText + case Photo + case Video + case Audio + case Emoji + case Location + case Url + case Phone + case Custom } - let system = MockUser(id: "000000", displayName: "System") - let nathan = MockUser(id: "000001", displayName: "Nathan Tannar") - let steven = MockUser(id: "000002", displayName: "Steven Deutsch") - let wu = MockUser(id: "000003", displayName: "Wu Zhong") + let system = MockUser(senderId: "000000", displayName: "System") + let nathan = MockUser(senderId: "000001", displayName: "Nathan Tannar") + let steven = MockUser(senderId: "000002", displayName: "Steven Deutsch") + let wu = MockUser(senderId: "000003", displayName: "Wu Zhong") lazy var senders = [nathan, steven, wu] @@ -141,13 +133,13 @@ final internal class SampleData { } func randomMessageType() -> MessageTypes { - let messageType = MessageTypes.random() - - if !UserDefaults.standard.bool(forKey: "\(messageType)" + " Messages") { - return randomMessageType() + var messageTypes = [MessageTypes]() + for type in MessageTypes.allCases { + if UserDefaults.standard.bool(forKey: "\(type.rawValue)" + " Messages") { + messageTypes.append(type) + } } - - return messageType + return messageTypes.random()! } func randomMessage(allowedSenders: [MockUser]) -> MockMessage { @@ -230,7 +222,7 @@ final internal class SampleData { let firstName = sender.displayName.components(separatedBy: " ").first let lastName = sender.displayName.components(separatedBy: " ").first let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")" - switch sender.id { + switch sender.senderId { case "000001": return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials) case "000002": diff --git a/Example/Sources/Models/MockUser.swift b/Example/Sources/Models/MockUser.swift index 19f6fe79..e4f2362f 100644 --- a/Example/Sources/Models/MockUser.swift +++ b/Example/Sources/Models/MockUser.swift @@ -26,6 +26,6 @@ import Foundation import MessageKit struct MockUser: SenderType, Equatable { - var id: String + var senderId: String var displayName: String } diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 3eb41451..1c7f9d3b 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -273,11 +273,14 @@ extension AdvancedExampleViewController: MessagesDisplayDelegate { } func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes + 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] + return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] } // MARK: - All Messages diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index 9cc5390e..a498e237 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -48,11 +48,14 @@ extension BasicExampleViewController: MessagesDisplayDelegate { } func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes + 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] + return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] } // MARK: - All Messages diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index f87101c5..d257e1c0 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -277,7 +277,19 @@ extension ChatViewController: MessageLabelDelegate { func didSelectTransitInformation(_ transitInformation: [String: String]) { print("TransitInformation Selected: \(transitInformation)") } - + + func didSelectHashtag(_ hashtag: String) { + print("Hashtag selected: \(hashtag)") + } + + func didSelectMention(_ mention: String) { + print("Mention selected: \(mention)") + } + + func didSelectCustom(_ pattern: String, match: String?) { + print("Custom data detector patter selected: \(pattern)") + } + } // MARK: - MessageInputBarDelegate diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 64c83376..8c005e87 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063421740DAD00CDB9DB /* TypingBubble.swift */; }; 38F8063721740DD500CDB9DB /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */; }; 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; + 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 5073C1152175BE750040EAD5 /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1142175BE750040EAD5 /* AudioItem.swift */; }; 5073C1192175BE960040EAD5 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */; }; 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */; }; @@ -154,6 +155,8 @@ 38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = ""; }; 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; + 38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; + 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; 5073C1142175BE750040EAD5 /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioItem.swift; sourceTree = ""; }; 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = ""; }; 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageSizeCalculator.swift; sourceTree = ""; }; @@ -255,12 +258,12 @@ path = Cells; sourceTree = ""; }; - 2EB618F11F846899007FBA0E /* Headers & Footers */ = { + 2EB618F11F846899007FBA0E /* HeadersFooters */ = { isa = PBXGroup; children = ( 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); - path = "Headers & Footers"; + path = HeadersFooters; sourceTree = ""; }; 5073C1212175C1580040EAD5 /* Resources */ = { @@ -424,7 +427,7 @@ isa = PBXGroup; children = ( 2EB618F01F84676A007FBA0E /* Cells */, - 2EB618F11F846899007FBA0E /* Headers & Footers */, + 2EB618F11F846899007FBA0E /* HeadersFooters */, B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */, 38F8063421740DAD00CDB9DB /* TypingBubble.swift */, diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index 33d6c24f..0dfd651a 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -24,19 +24,19 @@ import Foundation -public enum DetectorType { +public enum DetectorType: Hashable { case address case date case phoneNumber case url case transitInformation + case custom(NSRegularExpression) - // MARK: - Not supported yet - - //case mention - //case hashtag - //case custom + // 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 internal var textCheckingType: NSTextCheckingResult.CheckingType { switch self { @@ -45,6 +45,32 @@ public enum DetectorType { 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 + } + } + + ///The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted. + public var hashValue: Int { + return self.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 } } diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift index 22ea0a1e..28468785 100644 --- a/Sources/Models/Sender.swift +++ b/Sources/Models/Sender.swift @@ -33,15 +33,25 @@ public struct Sender: SenderType { /// The unique String identifier for the sender. /// /// Note: This value must be unique across all senders. - public let id: String + public let senderId: String + + @available(*, deprecated: 3.0.0, 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(id: String, displayName: String) { - self.id = id + public init(senderId: String, displayName: String) { + self.senderId = senderId self.displayName = displayName } + + @available(*, deprecated: 3.0.0, 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/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index 06a123e8..bdd4ee66 100644 --- a/Sources/Protocols/MessageLabelDelegate.swift +++ b/Sources/Protocols/MessageLabelDelegate.swift @@ -56,6 +56,25 @@ public protocol MessageLabelDelegate: AnyObject { /// - 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?) } @@ -71,4 +90,10 @@ public extension MessageLabelDelegate { func didSelectTransitInformation(_ transitInformation: [String: String]) {} + func didSelectMention(_ mention: String) {} + + func didSelectHashtag(_ hashtag: String) {} + + func didSelectCustom(_ pattern: String, match: String?) {} + } diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 40e7edc0..9db208f7 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -126,7 +126,7 @@ public protocol MessagesDataSource: AnyObject { public extension MessagesDataSource { func isFromCurrentSender(message: MessageType) -> Bool { - return message.sender.id == currentSender().id + return message.sender.senderId == currentSender().senderId } func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int { diff --git a/Sources/Protocols/SenderType.swift b/Sources/Protocols/SenderType.swift index 82f80e28..4c5c19a7 100644 --- a/Sources/Protocols/SenderType.swift +++ b/Sources/Protocols/SenderType.swift @@ -31,7 +31,7 @@ public protocol SenderType { /// The unique String identifier for the sender. /// /// Note: This value must be unique across all senders. - var id: String { get } + var senderId: String { get } /// The display name of a sender. var displayName: String { get } diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/HeadersFooters/MessageReusableView.swift similarity index 100% rename from Sources/Views/Headers & Footers/MessageReusableView.swift rename to Sources/Views/HeadersFooters/MessageReusableView.swift diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 061b6504..762ff044 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -49,7 +49,7 @@ open class MessageLabel: UILabel { return textStorage }() - private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:] + internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:] private var isConfiguring: Bool = false @@ -141,6 +141,12 @@ open class MessageLabel: UILabel { 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 { @@ -154,6 +160,12 @@ open class MessageLabel: UILabel { 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 @@ -283,6 +295,12 @@ open class MessageLabel: UILabel { return urlAttributes case .transitInformation: return transitInformationAttributes + case .mention: + return mentionAttributes + case .hashtag: + return hashtagAttributes + case .custom(let regex): + return customAttributes[regex] ?? MessageLabel.defaultAttributes } } @@ -313,10 +331,24 @@ open class MessageLabel: UILabel { private func parse(text: NSAttributedString) -> [NSTextCheckingResult] { guard enabledDetectors.isEmpty == false else { return [] } - let checkingTypes = enabledDetectors.reduce(0) { $0 | $1.textCheckingType.rawValue } - let detector = try? NSDataDetector(types: checkingTypes) let range = NSRange(location: 0, length: text.length) - let matches = detector?.matches(in: text.string, options: [], range: range) ?? [] + 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) + } guard enabledDetectors.contains(.url) else { return matches @@ -334,6 +366,15 @@ open class MessageLabel: UILabel { 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 setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) { guard checkingResults.isEmpty == false else { return } @@ -366,7 +407,13 @@ open class MessageLabel: UILabel { 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") } @@ -440,6 +487,16 @@ open class MessageLabel: UILabel { 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) + } } } @@ -462,13 +519,26 @@ open class MessageLabel: UILabel { 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) + } + } -private enum 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?) } diff --git a/Tests/ControllersTest/MessageLabelSpec.swift b/Tests/ControllersTest/MessageLabelSpec.swift index 76af3cea..72a9b0aa 100644 --- a/Tests/ControllersTest/MessageLabelSpec.swift +++ b/Tests/ControllersTest/MessageLabelSpec.swift @@ -38,7 +38,90 @@ final class MessageLabelSpec: QuickSpec { messageLabel = MessageLabel() } -// describe("text recognized by a DetectorType") { + describe("text recognized by a DetectorType") { + + let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"] + let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"] + + var detector: DetectorType! + var key: NSAttributedString.Key! + var attributes: [NSAttributedString.Key: Any]! + + context("Mention detection") { + + beforeEach { + detector = DetectorType.mention + key = NSAttributedString.Key(rawValue: "Mention") + attributes = [key: "MentionDetected"] + } + + it("match with multiples alpha and numerics") { + let text = mentionsList.joined(separator: " #test ") + self.set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches).to(equal(mentionsList)) + } + + it("is invalid") { + let invalids = hashtagsList.joined(separator: " ") + self.set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches.count).to(equal(0)) + } + + } + + context("Hashtag detection") { + + beforeEach { + detector = DetectorType.hashtag + key = NSAttributedString.Key(rawValue: "Hashtag") + attributes = [key: "HashtagDetected"] + } + + it("match with multiples alpha and numerics") { + let text = hashtagsList.joined(separator: " @test ") + self.set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches).to(equal(hashtagsList)) + } + + it("is invalid") { + let invalids = mentionsList.joined(separator: " ") + self.set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches.count).to(equal(0)) + } + + } + + context("Custom detection") { + + let shouldPass = ["1234", "1", "09876"] + let shouldFailed = ["abcd", "a", "!!!", ";"] + + beforeEach { + detector = DetectorType.custom(try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)) + key = NSAttributedString.Key(rawValue: "Custom") + attributes = [key: "CustomDetected"] + } + + it("must match with one or more numerics") { + let text = shouldPass.joined(separator: " ") + self.set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches).to(equal(shouldPass)) + } + + it("must failed with any non numerics characters") { + let invalids = shouldFailed.joined(separator: " ") + self.set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let matches = self.extractCustomDetectors(for: detector, with: messageLabel) + expect(matches.count).to(equal(0)) + } + + } + // context("address detection is enabled") { // it("applies addressAttributes to text") { // let expectedColor = UIColor.blue @@ -83,7 +166,7 @@ final class MessageLabelSpec: QuickSpec { // expect(textFont).to(equal(expectedFont)) // } // } -// } + } describe("the synchronization between text and attributedText") { context("when attributedText is set to a non-nil value") { @@ -237,6 +320,44 @@ final class MessageLabelSpec: QuickSpec { } } } + + // 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 + } + } // MARK: - Helpers diff --git a/Tests/Mocks/MockMessagesDataSource.swift b/Tests/Mocks/MockMessagesDataSource.swift index 31703420..4328b407 100644 --- a/Tests/Mocks/MockMessagesDataSource.swift +++ b/Tests/Mocks/MockMessagesDataSource.swift @@ -28,8 +28,10 @@ import Foundation class MockMessagesDataSource: MessagesDataSource { var messages: [MessageType] = [] - let senders: [MockUser] = [MockUser(id: "sender_1", displayName: "Sender 1"), - MockUser(id: "sender_2", displayName: "Sender 2")] + let senders: [MockUser] = [ + MockUser(senderId: "sender_1", displayName: "Sender 1"), + MockUser(senderId: "sender_2", displayName: "Sender 2") + ] var currentUser: MockUser { return senders[0] diff --git a/Tests/Mocks/MockUser.swift b/Tests/Mocks/MockUser.swift index 7e0f951c..a035f5e2 100644 --- a/Tests/Mocks/MockUser.swift +++ b/Tests/Mocks/MockUser.swift @@ -26,6 +26,6 @@ import Foundation @testable import MessageKit struct MockUser: SenderType { - var id: String + var senderId: String var displayName: String } diff --git a/Tests/SenderSpec.swift b/Tests/SenderSpec.swift index 2ed8839c..3175670d 100644 --- a/Tests/SenderSpec.swift +++ b/Tests/SenderSpec.swift @@ -32,16 +32,16 @@ final class SenderSpec: QuickSpec { describe("equality between two Senders") { context("they have the same id ") { it("should be equal") { - let sender1 = MockUser(id: "1", displayName: "Steven") - let sender2 = MockUser(id: "1", displayName: "Nathan") - expect(sender1.id == sender2.id).to(equal(true)) + let sender1 = MockUser(senderId: "1", displayName: "Steven") + let sender2 = MockUser(senderId: "1", displayName: "Nathan") + expect(sender1.senderId == sender2.senderId).to(equal(true)) } } context("they have a different id") { it("should not be equal") { - let sender1 = MockUser(id: "1", displayName: "Steven") - let sender2 = MockUser(id: "2", displayName: "Nathan") - expect(sender1.id == sender2.id).to(equal(false)) + let sender1 = MockUser(senderId: "1", displayName: "Steven") + let sender2 = MockUser(senderId: "2", displayName: "Nathan") + expect(sender1.senderId == sender2.senderId).to(equal(false)) } } }