Skip to content

Commit

Permalink
Merge pull request MessageKit#913 from JulienKode/CustomDetectorType
Browse files Browse the repository at this point in the history
[DetectorType][MessageLabel] Add custom, mention and hashtag detector type 👻
  • Loading branch information
nathantannar4 authored Mar 25, 2019
2 parents b542ac8 + 7555e0a commit a465437
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 28 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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)

Expand Down
14 changes: 6 additions & 8 deletions Example/Sources/Data Generation/SampleData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final internal class SampleData {
static let shared = SampleData()

private init() {}

enum MessageTypes: UInt32, CaseIterable {
case Text = 0
case AttributedText = 1
Expand All @@ -45,12 +45,10 @@ final internal class SampleData {
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)!
let randomIndex = Int(arc4random()) % MessageTypes.all.count
return all[randomIndex]
}

}

let system = MockUser(id: "000000", displayName: "System")
Expand Down Expand Up @@ -143,7 +141,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()
}

Expand All @@ -159,7 +157,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion Example/Sources/View Controllers/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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

Expand Down
38 changes: 32 additions & 6 deletions Sources/Models/DetectorType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}

Expand Down
25 changes: 25 additions & 0 deletions Sources/Protocols/MessageLabelDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?)

}

Expand All @@ -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?) {}

}
84 changes: 77 additions & 7 deletions Sources/Views/MessageLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}

}
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
}
}
}

Expand All @@ -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?)
}
Loading

0 comments on commit a465437

Please sign in to comment.