mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
[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]
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user