mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
8.7 KiB
Swift
229 lines
8.7 KiB
Swift
//
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
|
|
public class ConversationSearchResult: Comparable {
|
|
public let thread: ThreadViewModel
|
|
|
|
public let messageId: String?
|
|
public let messageDate: Date?
|
|
|
|
public let snippet: String?
|
|
|
|
private let sortKey: UInt64
|
|
|
|
init(thread: ThreadViewModel, sortKey: UInt64, messageId: String? = nil, messageDate: Date? = nil, snippet: String? = nil) {
|
|
self.thread = thread
|
|
self.sortKey = sortKey
|
|
self.messageId = messageId
|
|
self.messageDate = messageDate
|
|
self.snippet = snippet
|
|
}
|
|
|
|
// MARK: Comparable
|
|
|
|
public static func < (lhs: ConversationSearchResult, rhs: ConversationSearchResult) -> Bool {
|
|
return lhs.sortKey < rhs.sortKey
|
|
}
|
|
|
|
// MARK: Equatable
|
|
|
|
public static func == (lhs: ConversationSearchResult, rhs: ConversationSearchResult) -> Bool {
|
|
return lhs.thread.threadRecord.uniqueId == rhs.thread.threadRecord.uniqueId &&
|
|
lhs.messageId == rhs.messageId
|
|
}
|
|
}
|
|
|
|
public class ContactSearchResult: Comparable {
|
|
public let signalAccount: SignalAccount
|
|
public let contactsManager: ContactsManagerProtocol
|
|
|
|
public var recipientId: String {
|
|
return signalAccount.recipientId
|
|
}
|
|
|
|
init(signalAccount: SignalAccount, contactsManager: ContactsManagerProtocol) {
|
|
self.signalAccount = signalAccount
|
|
self.contactsManager = contactsManager
|
|
}
|
|
|
|
// MARK: Comparable
|
|
|
|
public static func < (lhs: ContactSearchResult, rhs: ContactSearchResult) -> Bool {
|
|
return lhs.contactsManager.compare(signalAccount: lhs.signalAccount, with: rhs.signalAccount) == .orderedAscending
|
|
}
|
|
|
|
// MARK: Equatable
|
|
|
|
public static func == (lhs: ContactSearchResult, rhs: ContactSearchResult) -> Bool {
|
|
return lhs.recipientId == rhs.recipientId
|
|
}
|
|
}
|
|
|
|
public class SearchResultSet {
|
|
public let searchText: String
|
|
public let conversations: [ConversationSearchResult]
|
|
public let contacts: [ContactSearchResult]
|
|
public let messages: [ConversationSearchResult]
|
|
|
|
public init(searchText: String, conversations: [ConversationSearchResult], contacts: [ContactSearchResult], messages: [ConversationSearchResult]) {
|
|
self.searchText = searchText
|
|
self.conversations = conversations
|
|
self.contacts = contacts
|
|
self.messages = messages
|
|
}
|
|
|
|
public class var empty: SearchResultSet {
|
|
return SearchResultSet(searchText: "", conversations: [], contacts: [], messages: [])
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
return conversations.isEmpty && contacts.isEmpty && messages.isEmpty
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public class ConversationSearcher: NSObject {
|
|
|
|
private let finder: FullTextSearchFinder
|
|
|
|
@objc
|
|
public static let shared: ConversationSearcher = ConversationSearcher()
|
|
override private init() {
|
|
finder = FullTextSearchFinder()
|
|
super.init()
|
|
}
|
|
|
|
public func results(searchText: String,
|
|
transaction: YapDatabaseReadTransaction,
|
|
contactsManager: ContactsManagerProtocol) -> SearchResultSet {
|
|
|
|
var conversations: [ConversationSearchResult] = []
|
|
var contacts: [ContactSearchResult] = []
|
|
var messages: [ConversationSearchResult] = []
|
|
|
|
var existingConversationRecipientIds: Set<String> = Set()
|
|
|
|
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
|
|
|
|
if let thread = match as? TSThread {
|
|
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
|
let sortKey = NSDate.ows_millisecondsSince1970(for: threadViewModel.lastMessageDate)
|
|
let searchResult = ConversationSearchResult(thread: threadViewModel, sortKey: sortKey)
|
|
|
|
if let contactThread = thread as? TSContactThread {
|
|
let recipientId = contactThread.contactIdentifier()
|
|
existingConversationRecipientIds.insert(recipientId)
|
|
}
|
|
|
|
conversations.append(searchResult)
|
|
} else if let message = match as? TSMessage {
|
|
let thread = message.thread(with: transaction)
|
|
|
|
let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
|
let sortKey = message.timestamp
|
|
let searchResult = ConversationSearchResult(thread: threadViewModel,
|
|
sortKey: sortKey,
|
|
messageId: message.uniqueId,
|
|
messageDate: NSDate.ows_date(withMillisecondsSince1970: message.timestamp),
|
|
snippet: snippet)
|
|
|
|
messages.append(searchResult)
|
|
} else if let signalAccount = match as? SignalAccount {
|
|
let searchResult = ContactSearchResult(signalAccount: signalAccount, contactsManager: contactsManager)
|
|
contacts.append(searchResult)
|
|
} else {
|
|
owsFailDebug("unhandled item: \(match)")
|
|
}
|
|
}
|
|
|
|
// Only show contacts which were not included in an existing 1:1 conversation.
|
|
var otherContacts: [ContactSearchResult] = contacts.filter { !existingConversationRecipientIds.contains($0.recipientId) }
|
|
|
|
// Order the conversation and message results in reverse chronological order.
|
|
// The contact results are pre-sorted by display name.
|
|
conversations.sort(by: >)
|
|
messages.sort(by: >)
|
|
// Order "other" contact results by display name.
|
|
otherContacts.sort()
|
|
|
|
return SearchResultSet(searchText: searchText, conversations: conversations, contacts: otherContacts, messages: messages)
|
|
}
|
|
|
|
@objc(filterThreads:withSearchText:)
|
|
public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
|
|
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
|
return threads
|
|
}
|
|
|
|
return threads.filter { thread in
|
|
switch thread {
|
|
case let groupThread as TSGroupThread:
|
|
return self.groupThreadSearcher.matches(item: groupThread, query: searchText)
|
|
case let contactThread as TSContactThread:
|
|
return self.contactThreadSearcher.matches(item: contactThread, query: searchText)
|
|
default:
|
|
owsFailDebug("Unexpected thread type: \(thread)")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc(filterGroupThreads:withSearchText:)
|
|
public func filterGroupThreads(_ groupThreads: [TSGroupThread], searchText: String) -> [TSGroupThread] {
|
|
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
|
return groupThreads
|
|
}
|
|
|
|
return groupThreads.filter { groupThread in
|
|
return self.groupThreadSearcher.matches(item: groupThread, query: searchText)
|
|
}
|
|
}
|
|
|
|
@objc(filterSignalAccounts:withSearchText:)
|
|
public func filterSignalAccounts(_ signalAccounts: [SignalAccount], searchText: String) -> [SignalAccount] {
|
|
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
|
return signalAccounts
|
|
}
|
|
|
|
return signalAccounts.filter { signalAccount in
|
|
self.signalAccountSearcher.matches(item: signalAccount, query: searchText)
|
|
}
|
|
}
|
|
|
|
// MARK: Searchers
|
|
|
|
private lazy var groupThreadSearcher: Searcher<TSGroupThread> = Searcher { (groupThread: TSGroupThread) in
|
|
let groupName = groupThread.groupModel.groupName
|
|
let memberStrings = groupThread.groupModel.groupMemberIds.map { recipientId in
|
|
self.indexingString(recipientId: recipientId)
|
|
}.joined(separator: " ")
|
|
|
|
return "\(memberStrings) \(groupName ?? "")"
|
|
}
|
|
|
|
private lazy var contactThreadSearcher: Searcher<TSContactThread> = Searcher { (contactThread: TSContactThread) in
|
|
let recipientId = contactThread.contactIdentifier()
|
|
return self.indexingString(recipientId: recipientId)
|
|
}
|
|
|
|
private lazy var signalAccountSearcher: Searcher<SignalAccount> = Searcher { (signalAccount: SignalAccount) in
|
|
let recipientId = signalAccount.recipientId
|
|
return self.indexingString(recipientId: recipientId)
|
|
}
|
|
|
|
private var contactsManager: OWSContactsManager {
|
|
return Environment.shared.contactsManager
|
|
}
|
|
|
|
private func indexingString(recipientId: String) -> String {
|
|
let contactName = contactsManager.displayName(forPhoneIdentifier: recipientId)
|
|
let profileName = contactsManager.profileName(forRecipientId: recipientId)
|
|
|
|
return "\(recipientId) \(contactName) \(profileName ?? "")"
|
|
}
|
|
}
|