From 77e9ccda4697d4cafb9854dffc88ffdbdbed7a71 Mon Sep 17 00:00:00 2001 From: Julien K Date: Tue, 16 Oct 2018 16:26:57 +0200 Subject: [PATCH 1/8] [DetectorType] add .mention, .hashtag and .custon() to detect your own pattern [DetectorType] Put mention and hashtag as var [CHANGELOG] Update [DetectorType] Update to have at least 4 characters and more [CHANGELOG] Update [DetectorType] [MessageLabel] run NSDataDetector once [MessageLabel] Run NSDataDetector once [Testing] Add testing and pass rangesOfDetectors as internal [Refactoring] [Filter] only .custom [Examples] --- CHANGELOG.md | 3 + .../Sources/Data Generation/SampleData.swift | 6 +- .../AdvancedExampleViewController.swift | 7 +- .../BasicExampleViewController.swift | 7 +- Sources/Models/DetectorType.swift | 38 +++++- Sources/Protocols/MessageLabelDelegate.swift | 25 ++++ Sources/Views/MessageLabel.swift | 93 ++++++++++++- Tests/ControllersTest/MessageLabelSpec.swift | 125 +++++++++++++++++- 8 files changed, 284 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4602998d..9e0b1329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,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 63614a73..50b3dfa3 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -46,7 +46,7 @@ final internal class SampleData { let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")] - let messageTypes = ["Text", "Text", "Text", "AttributedText", "Location", "Photo", "Emoji", "Video", "URL", "Phone", "Custom"] + let messageTypes = ["Text", "Text", "Text", "AttributedText", "Location", "Photo", "Emoji", "Video", "URL", "Phone", "Mention", "Hashtag", "Custom"] let emojis = [ "👍", @@ -150,6 +150,10 @@ final internal class SampleData { return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date) case "Phone": return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date) + case "Mention": + return MockMessage(text: "@messagekit", sender: sender, messageId: uniqueID, date: date) + case "Hashtag": + return MockMessage(text: "#messagekit", sender: sender, messageId: uniqueID, date: date) case "Custom": return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date) default: diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 32c8761f..3f3c8432 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -258,11 +258,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 0d7fc717..b2fd0e96 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -49,11 +49,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/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index a49b7b6d..d3d43f2e 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(regex: NSRegularExpression) - // MARK: - Not supported yet - - //case mention - //case hashtag - //case custom + // swiftlint:disable force_try + public static var hashtag = DetectorType.custom(regex: try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) + public static var mention = DetectorType.custom(regex: 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/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index e84ea622..46f5759d 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/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 9b04d5bd..d72a0ffc 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 @@ -134,6 +134,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 { @@ -147,6 +153,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 @@ -276,6 +288,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 } } @@ -301,10 +319,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 @@ -322,6 +354,24 @@ open class MessageLabel: UILabel { return results } + /** + Take a custom detector and apply the matching to the text + + - Parameters: detector: `DetectorType` that you want to execute + - Parameters: text: where we apply the regular expression + - Parameters: range: where we apply the regular expression + + - Returns: an array of `NSTextCheckingResult` that contains all maching for this detector or nil if it fails + */ + 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 } @@ -354,7 +404,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: 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") } @@ -428,6 +484,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: + handleHashtag(match) + default: + handleCustom(pattern, match: match) + } } } @@ -450,13 +516,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 abf28a00..5d62e52f 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(regex: 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 From 8fb0600c12d6f8926301df269635d6ce06305564 Mon Sep 17 00:00:00 2001 From: Julien K Date: Wed, 31 Oct 2018 20:40:23 +0100 Subject: [PATCH 2/8] Fix handleMention call --- Sources/Views/MessageLabel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index d72a0ffc..7b51e342 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -490,7 +490,7 @@ open class MessageLabel: UILabel { case .hashtag: handleHashtag(match) case .mention: - handleHashtag(match) + handleMention(match) default: handleCustom(pattern, match: match) } From 9a64ebf6216243897af334b10e28d2d5b95e72a7 Mon Sep 17 00:00:00 2001 From: Julien K Date: Thu, 1 Nov 2018 11:25:45 +0100 Subject: [PATCH 3/8] [Examples] Add delegate methods --- .../View Controllers/ChatViewController.swift | 14 +++++++++++++- .../View Controllers/SettingsViewController.swift | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index a8de9166..412d796a 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -223,7 +223,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/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index 882e0f9e..3db827a2 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -34,7 +34,7 @@ final internal class SettingsViewController: UITableViewController { return .lightContent } - let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages"] + let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages", "Hashtag Messages", "Mention Messages"] // MARK: - Picker From d238e09155f9347c601dcdaaf4a0cc25f468510b Mon Sep 17 00:00:00 2001 From: Julien K Date: Fri, 2 Nov 2018 11:17:39 +0100 Subject: [PATCH 4/8] [Update] --- Sources/Models/DetectorType.swift | 6 +++--- Sources/Views/MessageLabel.swift | 13 ++----------- Tests/ControllersTest/MessageLabelSpec.swift | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index d3d43f2e..0842f41e 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -31,11 +31,11 @@ public enum DetectorType: Hashable { case phoneNumber case url case transitInformation - case custom(regex: NSRegularExpression) + case custom(NSRegularExpression) // swiftlint:disable force_try - public static var hashtag = DetectorType.custom(regex: try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) - public static var mention = DetectorType.custom(regex: try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: [])) + 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 { diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 402329b7..49664044 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -332,7 +332,7 @@ open class MessageLabel: UILabel { private func parse(text: NSAttributedString) -> [NSTextCheckingResult] { guard enabledDetectors.isEmpty == false else { return [] } let range = NSRange(location: 0, length: text.length) - var matches = [NSTextCheckingResult]() + var matches = [NSTextCheckingResult]() // Get matches of all .custom DetectorType and add it to matches array let regexs = enabledDetectors @@ -366,15 +366,6 @@ open class MessageLabel: UILabel { return results } - /** - Take a custom detector and apply the matching to the text - - - Parameters: detector: `DetectorType` that you want to execute - - Parameters: text: where we apply the regular expression - - Parameters: range: where we apply the regular expression - - - Returns: an array of `NSTextCheckingResult` that contains all maching for this detector or nil if it fails - */ private func parseForMatches(with detector: DetectorType, in text: NSAttributedString, for range: NSRange) -> [NSTextCheckingResult] { switch detector { case .custom(let regex): @@ -418,7 +409,7 @@ open class MessageLabel: UILabel { 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: regex) + 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) diff --git a/Tests/ControllersTest/MessageLabelSpec.swift b/Tests/ControllersTest/MessageLabelSpec.swift index 5d62e52f..cd16342a 100644 --- a/Tests/ControllersTest/MessageLabelSpec.swift +++ b/Tests/ControllersTest/MessageLabelSpec.swift @@ -101,7 +101,7 @@ final class MessageLabelSpec: QuickSpec { let shouldFailed = ["abcd", "a", "!!!", ";"] beforeEach { - detector = DetectorType.custom(regex: try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)) + detector = DetectorType.custom(try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)) key = NSAttributedString.Key(rawValue: "Custom") attributes = [key: "CustomDetected"] } From d7df6fd5a59ed6b66e9c4967d8ac9c9214f629e6 Mon Sep 17 00:00:00 2001 From: Julien K Date: Fri, 2 Nov 2018 11:59:53 +0100 Subject: [PATCH 5/8] [Examples] Update MessageTypes enum --- .../Sources/Data Generation/SampleData.swift | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 14b24ed4..90ad1ecf 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -31,26 +31,38 @@ final internal class SampleData { private init() {} - enum MessageTypes: UInt32, CaseIterable { - case Text = 0 - case AttributedText = 1 - case Photo = 2 - case Video = 3 - case Emoji = 4 - case Location = 5 - case Url = 6 - case Phone = 7 - case Hashtag = 8 - case Mention = 9 - case Custom = 10 + enum MessageTypes: String, CaseIterable { + case text = "Text" + case attributedText = "AttributedText" + case photo = "Photo" + case video = "Video" + case emoji = "Emoji" + case location = "Location" + case url = "Url" + case phone = "Phone" + case hashtag = "Hashtag" + case mention = "Mention" + case custom = "Custom" + + static let all: [MessageTypes] = [ + .text, + .attributedText, + .photo, + .video, + .emoji, + .location, + .url, + .phone, + .hashtag, + .mention, + .custom + ] static func random() -> MessageTypes { - // Update as new enumerations are added - let maxValue = Custom.rawValue - - let rand = arc4random_uniform(maxValue+1) - return MessageTypes(rawValue: rand)! + let randomIndex = Int(arc4random()) % MessageTypes.all.count + return all[randomIndex] } + } let system = Sender(id: "000000", displayName: "System") @@ -140,7 +152,7 @@ final internal class SampleData { func randomMessageType() -> MessageTypes { let messageType = MessageTypes.random() - if !UserDefaults.standard.bool(forKey: "\(messageType)" + " Messages") { + if !UserDefaults.standard.bool(forKey: "\(messageType.rawValue)" + " Messages") { return randomMessageType() } @@ -156,36 +168,36 @@ final internal class SampleData { let date = dateAddingRandomTime() switch randomMessageType() { - case .Text: + case .text: let randomSentence = Lorem.sentence() return MockMessage(text: randomSentence, sender: sender, messageId: uniqueID, date: date) - case .AttributedText: + case .attributedText: let randomSentence = Lorem.sentence() let attributedText = attributedString(with: randomSentence) return MockMessage(attributedText: attributedText, sender: senders[randomNumberSender], messageId: uniqueID, date: date) - case .Photo: + case .photo: let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) let image = messageImages[randomNumberImage] return MockMessage(image: image, sender: sender, messageId: uniqueID, date: date) - case .Video: + case .video: let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) let image = messageImages[randomNumberImage] return MockMessage(thumbnail: image, sender: sender, messageId: uniqueID, date: date) - case .Emoji: + case .emoji: let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) return MockMessage(emoji: emojis[randomNumberEmoji], sender: sender, messageId: uniqueID, date: date) - case .Location: + case .location: let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) return MockMessage(location: locations[randomNumberLocation], sender: sender, messageId: uniqueID, date: date) - case .Url: + case .url: return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date) - case .Phone: + case .phone: return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date) - case .Mention: + case .mention: return MockMessage(text: "@messagekit", sender: sender, messageId: uniqueID, date: date) - case .Hashtag: + case .hashtag: return MockMessage(text: "#messagekit", sender: sender, messageId: uniqueID, date: date) - case .Custom: + case .custom: return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date) } } From b542ac88ee83f2185e6b007676e4c20a33eeedf9 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 24 Mar 2019 17:38:20 -0700 Subject: [PATCH 6/8] Rename `id` to `senderId` in `SenderType` --- Sources/Models/Sender.swift | 16 +++++++++++++--- Sources/Protocols/MessagesDataSource.swift | 2 +- Sources/Protocols/SenderType.swift | 2 +- .../MessagesViewControllerSpec.swift | 2 +- Tests/Mocks/MockMessagesDataSource.swift | 6 ++++-- Tests/Mocks/MockUser.swift | 2 +- Tests/SenderSpec.swift | 12 ++++++------ 7 files changed, 27 insertions(+), 15 deletions(-) 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/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index a725459d..0d219ec9 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -118,7 +118,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/Tests/ControllersTest/MessagesViewControllerSpec.swift b/Tests/ControllersTest/MessagesViewControllerSpec.swift index 59646326..77c4de4c 100644 --- a/Tests/ControllersTest/MessagesViewControllerSpec.swift +++ b/Tests/ControllersTest/MessagesViewControllerSpec.swift @@ -24,7 +24,7 @@ import Quick import Nimble -import MessageInputBar +import InputBarAccessoryView @testable import MessageKit //swiftlint:disable function_body_length 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)) } } } From c04fff5ba91a299f88dd1997906330a3ad692e13 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 24 Mar 2019 17:41:49 -0700 Subject: [PATCH 7/8] Remove space in folder name --- MessageKit.xcodeproj/project.pbxproj | 11 +++++------ .../MessageReusableView.swift | 0 2 files changed, 5 insertions(+), 6 deletions(-) rename Sources/Views/{Headers & Footers => HeadersFooters}/MessageReusableView.swift (100%) diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 6ed27369..a2de4844 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -35,12 +35,11 @@ 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; - 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 383B9EB121728BAD008AB91A /* SenderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383B9EB021728BAD008AB91A /* SenderType.swift */; }; 38A2230F221FB8A300D14DAF /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */; }; 38A223112223493500D14DAF /* InputBarAccessoryView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38A223102223493400D14DAF /* InputBarAccessoryView.framework */; }; - 38C2AE7C20D4878D00F8079E /* MessageInputBar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */; }; 38F8062F2173CD8F00CDB9DB /* MockUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8062D2173CD4300CDB9DB /* MockUser.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 */; }; @@ -143,8 +142,8 @@ 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; 38A223102223493400D14DAF /* InputBarAccessoryView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputBarAccessoryView.framework; path = Carthage/Build/iOS/InputBarAccessoryView.framework; sourceTree = ""; }; 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; 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 = ""; }; @@ -245,12 +244,12 @@ path = Cells; sourceTree = ""; }; - 2EB618F11F846899007FBA0E /* Headers & Footers */ = { + 2EB618F11F846899007FBA0E /* HeadersFooters */ = { isa = PBXGroup; children = ( 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); - path = "Headers & Footers"; + path = HeadersFooters; sourceTree = ""; }; 5073C1212175C1580040EAD5 /* Resources */ = { @@ -414,7 +413,7 @@ isa = PBXGroup; children = ( 2EB618F01F84676A007FBA0E /* Cells */, - 2EB618F11F846899007FBA0E /* Headers & Footers */, + 2EB618F11F846899007FBA0E /* HeadersFooters */, B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, 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 From a058885b37eac30a6e269fcd388f23dc0e4e894d Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 24 Mar 2019 18:34:43 -0700 Subject: [PATCH 8/8] Fix example app --- .../Sources/Data Generation/SampleData.swift | 52 ++++++++----------- Example/Sources/Models/MockUser.swift | 2 +- .../SettingsViewController.swift | 1 - 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 44eaf155..2e353d55 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -32,29 +32,23 @@ final internal class 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 { - let randomIndex = Int(arc4random()) % MessageTypes.all.count - return all[randomIndex] - } - + 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] @@ -139,13 +133,13 @@ final internal class SampleData { } func randomMessageType() -> MessageTypes { - let messageType = MessageTypes.random() - - if !UserDefaults.standard.bool(forKey: "\(messageType.rawValue)" + " 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 { @@ -157,7 +151,7 @@ final internal class SampleData { let date = dateAddingRandomTime() switch randomMessageType() { - case .text: + case .Text: let randomSentence = Lorem.sentence() return MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date) case .AttributedText: @@ -228,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 de17c097..def55314 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/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index ae0b3d40..7c39887e 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -34,7 +34,6 @@ final internal class SettingsViewController: UITableViewController { } let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Audio Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages"] - // MARK: - Picker