Signal-iOS/SignalMessaging/ViewControllers/ConversationPicker/ConversationPicker.swift
2021-09-09 12:55:25 -03:00

875 lines
33 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
public protocol ConversationPickerDelegate: AnyObject {
func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController)
func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController)
func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool
func conversationPickerDidCancel(_ conversationPickerViewController: ConversationPickerViewController)
func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode
func conversationPickerDidBeginEditingText()
func conversationPickerSearchBarActiveDidChange(_ conversationPickerViewController: ConversationPickerViewController)
}
// MARK: -
@objc
open class ConversationPickerViewController: OWSTableViewController2 {
public weak var pickerDelegate: ConversationPickerDelegate?
private let kMaxPickerSelection = 5
public let selection: ConversationPickerSelection
private let footerView = ApprovalFooterView()
fileprivate lazy var searchBar: OWSSearchBar = {
let searchBar = OWSSearchBar()
searchBar.placeholder = CommonStrings.searchPlaceholder
searchBar.delegate = self
return searchBar
}()
private let searchBarWrapper: UIStackView = {
let searchBarWrapper = UIStackView()
searchBarWrapper.axis = .vertical
searchBarWrapper.alignment = .fill
return searchBarWrapper
}()
public var textInput: String? {
footerView.textInput
}
private var conversationCollection: ConversationCollection = .empty {
didSet {
updateTableContents()
}
}
public var approvalTextMode: ApprovalFooterView.ApprovalTextMode {
get { footerView.approvalTextMode }
set { footerView.approvalTextMode = newValue }
}
public init(selection: ConversationPickerSelection) {
self.selection = selection
super.init()
self.selectionBehavior = .toggleSelectionWithAction
self.shouldAvoidKeyboard = true
searchBarWrapper.addArrangedSubview(searchBar)
self.topHeader = searchBarWrapper
self.bottomFooter = footerView
selection.delegate = self
}
private var approvalMode: ApprovalMode {
pickerDelegate?.approvalMode(self) ?? .send
}
public func updateApprovalMode() { footerView.updateContents() }
public var shouldShowSearchBar: Bool = true {
didSet {
if isViewLoaded {
ensureSearchBarVisibility()
}
}
}
public var shouldHideRecentConversationsTitle = false {
didSet {
if isViewLoaded {
updateTableContents()
}
}
}
public var shouldHideSearchBarIfCancelled = false
private func ensureSearchBarVisibility() {
AssertIsOnMainThread()
searchBar.isHidden = !shouldShowSearchBar
}
public func selectSearchBar() {
AssertIsOnMainThread()
shouldShowSearchBar = true
searchBar.becomeFirstResponder()
}
open override func viewDidLoad() {
super.viewDidLoad()
if pickerDelegate?.conversationPickerCanCancel(self) ?? false {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
self.navigationItem.leftBarButtonItem = cancelButton
}
ensureSearchBarVisibility()
title = Strings.title
tableView.allowsMultipleSelection = true
tableView.register(ConversationPickerCell.self, forCellReuseIdentifier: ConversationPickerCell.reuseIdentifier)
footerView.delegate = self
conversationCollection = buildConversationCollection()
NotificationCenter.default.addObserver(self,
selector: #selector(blockListDidChange),
name: BlockingManager.blockListDidChange,
object: nil)
}
// MARK: - ConversationCollection
private func restoreSelection() {
AssertIsOnMainThread()
tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: false) }
for selectedConversation in selection.conversations {
guard let index = conversationCollection.indexPath(conversation: selectedConversation) else {
// This can happen when restoring selection while the currently displayed results
// are filtered.
continue
}
tableView.selectRow(at: index, animated: false, scrollPosition: .none)
}
updateUIForCurrentSelection(animated: false)
}
func buildSearchResults(searchText: String) -> Promise<ComposeScreenSearchResultSet?> {
guard searchText.count > 1 else {
return Promise.value(nil)
}
return firstly(on: .global()) {
Self.databaseStorage.read { transaction in
self.fullTextSearcher.searchForComposeScreen(searchText: searchText,
omitLocalUser: false,
transaction: transaction)
}
}
}
func buildGroupItem(_ groupThread: TSGroupThread, transaction: SDSAnyReadTransaction) -> GroupConversationItem {
let isBlocked = self.blockingManager.isThreadBlocked(groupThread)
let dmConfig = groupThread.disappearingMessagesConfiguration(with: transaction)
return GroupConversationItem(groupThreadId: groupThread.uniqueId,
isBlocked: isBlocked,
disappearingMessagesConfig: dmConfig)
}
func buildContactItem(_ address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> ContactConversationItem {
let isBlocked = self.blockingManager.isAddressBlocked(address)
let dmConfig = TSContactThread.getWithContactAddress(address, transaction: transaction)?.disappearingMessagesConfiguration(with: transaction)
let contactName = contactsManager.displayName(for: address,
transaction: transaction)
let comparableName = contactsManager.comparableName(for: address,
transaction: transaction)
return ContactConversationItem(address: address,
isBlocked: isBlocked,
disappearingMessagesConfig: dmConfig,
contactName: contactName,
comparableName: comparableName)
}
fileprivate func buildConversationCollection() -> ConversationCollection {
self.databaseStorage.read { transaction in
var pinnedItemsByThreadId: [String: RecentConversationItem] = [:]
var recentItems: [RecentConversationItem] = []
var contactItems: [ContactConversationItem] = []
var groupItems: [GroupConversationItem] = []
var seenAddresses: Set<SignalServiceAddress> = Set()
let pinnedThreadIds = PinnedThreadManager.pinnedThreadIds
// We append any pinned threads at the start of the "recent"
// section, so we decrease our maximum recent items based
// on how many threads are currently pinned.
let maxRecentCount = 25 - pinnedThreadIds.count
let addThread = { (thread: TSThread) -> Void in
guard thread.canSendChatMessagesToThread(ignoreAnnouncementOnly: true) else {
return
}
switch thread {
case let contactThread as TSContactThread:
let item = self.buildContactItem(contactThread.contactAddress, transaction: transaction)
seenAddresses.insert(contactThread.contactAddress)
if pinnedThreadIds.contains(thread.uniqueId) {
let recentItem = RecentConversationItem(backingItem: .contact(item))
pinnedItemsByThreadId[thread.uniqueId] = recentItem
} else if recentItems.count < maxRecentCount {
let recentItem = RecentConversationItem(backingItem: .contact(item))
recentItems.append(recentItem)
} else {
contactItems.append(item)
}
case let groupThread as TSGroupThread:
guard groupThread.isLocalUserFullMember else {
return
}
let item = self.buildGroupItem(groupThread, transaction: transaction)
if pinnedThreadIds.contains(thread.uniqueId) {
let recentItem = RecentConversationItem(backingItem: .group(item))
pinnedItemsByThreadId[thread.uniqueId] = recentItem
} else if recentItems.count < maxRecentCount {
let recentItem = RecentConversationItem(backingItem: .group(item))
recentItems.append(recentItem)
} else {
groupItems.append(item)
}
default:
owsFailDebug("unexpected thread: \(thread.uniqueId)")
}
}
try! AnyThreadFinder().enumerateVisibleThreads(isArchived: false, transaction: transaction) { thread in
addThread(thread)
}
try! AnyThreadFinder().enumerateVisibleThreads(isArchived: true, transaction: transaction) { thread in
addThread(thread)
}
SignalAccount.anyEnumerate(transaction: transaction) { signalAccount, _ in
let address = signalAccount.recipientAddress
guard !seenAddresses.contains(address) else {
return
}
seenAddresses.insert(address)
let contactItem = self.buildContactItem(address, transaction: transaction)
contactItems.append(contactItem)
}
contactItems.sort()
let pinnedItems = pinnedItemsByThreadId.sorted { lhs, rhs in
guard let lhsIndex = pinnedThreadIds.firstIndex(of: lhs.key),
let rhsIndex = pinnedThreadIds.firstIndex(of: rhs.key) else {
owsFailDebug("Unexpectedly have pinned item without pinned thread id")
return false
}
return lhsIndex < rhsIndex
}.map { $0.value }
return ConversationCollection(contactConversations: contactItems,
recentConversations: pinnedItems + recentItems,
groupConversations: groupItems,
isSearchResults: false)
}
}
fileprivate func buildConversationCollection(searchResults: ComposeScreenSearchResultSet?) -> Promise<ConversationCollection> {
guard let searchResults = searchResults else {
return Promise.value(buildConversationCollection())
}
return firstly(on: .global()) {
Self.databaseStorage.read { transaction in
let groupItems = searchResults.groupThreads.compactMap { groupThread -> GroupConversationItem? in
guard groupThread.canSendChatMessagesToThread(ignoreAnnouncementOnly: true) else {
return nil
}
return self.buildGroupItem(groupThread, transaction: transaction)
}
let contactItems = searchResults.signalAccounts.map { self.buildContactItem($0.recipientAddress, transaction: transaction) }
return ConversationCollection(contactConversations: contactItems,
recentConversations: [],
groupConversations: groupItems,
isSearchResults: true)
}
}
}
public func conversation(for indexPath: IndexPath) -> ConversationItem? {
conversationCollection.conversation(for: indexPath)
}
public func conversation(for thread: TSThread) -> ConversationItem? {
conversationCollection.conversation(for: thread)
}
// MARK: - Button Actions
@objc func onTouchCancelButton() {
pickerDelegate?.conversationPickerDidCancel(self)
}
@objc
private func blockListDidChange(_ notification: NSNotification) {
AssertIsOnMainThread()
self.conversationCollection = buildConversationCollection()
}
private func updateTableContents() {
AssertIsOnMainThread()
self.defaultSeparatorInsetLeading = (OWSTableViewController2.cellHInnerMargin +
CGFloat(ContactCellView.avatarDiameterPoints) +
ContactCellView.avatarTextHSpacing)
let conversationCollection = self.conversationCollection
let contents = OWSTableContents()
var hasContents = false
// Recents Section
do {
let section = OWSTableSection()
if !conversationCollection.recentConversations.isEmpty {
if !shouldHideRecentConversationsTitle {
section.headerTitle = Strings.recentsSection
}
addConversations(toSection: section,
conversations: conversationCollection.recentConversations)
hasContents = true
}
contents.addSection(section)
}
// Contacts Section
do {
let section = OWSTableSection()
if !conversationCollection.contactConversations.isEmpty {
section.headerTitle = Strings.signalContactsSection
addConversations(toSection: section,
conversations: conversationCollection.contactConversations)
hasContents = true
}
contents.addSection(section)
}
// Groups Section
do {
let section = OWSTableSection()
if !conversationCollection.groupConversations.isEmpty {
section.headerTitle = Strings.groupsSection
addConversations(toSection: section,
conversations: conversationCollection.groupConversations)
hasContents = true
}
contents.addSection(section)
}
// "No matches" Section
if conversationCollection.isSearchResults,
!hasContents {
let section = OWSTableSection()
section.add(.label(withText: NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS",
comment: "keyboard toolbar label when no messages match the search string")))
contents.addSection(section)
}
self.contents = contents
restoreSelection()
}
private func addConversations(toSection section: OWSTableSection,
conversations: [ConversationItem]) {
for conversation in conversations {
section.add(OWSTableItem(dequeueCellBlock: { tableView in
guard let cell = tableView.dequeueReusableCell(withIdentifier: ConversationPickerCell.reuseIdentifier) as? ConversationPickerCell else {
owsFailDebug("Missing cell.")
return UITableViewCell()
}
Self.databaseStorage.read { transaction in
cell.configure(conversationItem: conversation, transaction: transaction)
}
return cell
},
actionBlock: { [weak self] in
self?.didToggleSection(conversation: conversation)
}))
}
}
public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard let indexPath = super.tableView(tableView, willSelectRowAt: indexPath) else {
return nil
}
guard selection.conversations.count < kMaxPickerSelection else {
showTooManySelectedToast()
return nil
}
guard let conversation = conversation(for: indexPath) else {
owsFailDebug("item was unexpectedly nil")
return nil
}
guard !conversation.isBlocked else {
showUnblockUI(conversation: conversation)
return nil
}
return indexPath
}
private func showUnblockUI(conversation: ConversationItem) {
switch conversation.messageRecipient {
case .contact(let address):
BlockListUIUtils.showUnblockAddressActionSheet(address,
from: self) { isStillBlocked in
AssertIsOnMainThread()
guard !isStillBlocked else {
return
}
self.conversationCollection = self.buildConversationCollection()
}
case .group(let groupThreadId):
guard let groupThread = databaseStorage.read(block: { transaction in
return TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: transaction)
}) else {
owsFailDebug("Missing group thread for blocked thread")
return
}
BlockListUIUtils.showUnblockThreadActionSheet(groupThread,
from: self) { isStillBlocked in
AssertIsOnMainThread()
guard !isStillBlocked else {
return
}
self.conversationCollection = self.buildConversationCollection()
}
}
}
fileprivate func didToggleSection(conversation: ConversationItem) {
AssertIsOnMainThread()
if selection.isSelected(conversation: conversation) {
didDeselect(conversation: conversation)
} else {
didSelect(conversation: conversation)
}
}
private func didSelect(conversation: ConversationItem) {
AssertIsOnMainThread()
let isBlocked: Bool = databaseStorage.write { transaction in
guard let thread = conversation.thread(transaction: transaction) else {
return false
}
return !thread.canSendChatMessagesToThread(ignoreAnnouncementOnly: false)
}
guard !isBlocked else {
restoreSelection()
showBlockedByAnnouncementOnlyToast()
return
}
selection.add(conversation)
updateUIForCurrentSelection(animated: true)
}
private func showBlockedByAnnouncementOnlyToast() {
Logger.info("")
let toastFormat = NSLocalizedString("CONVERSATION_PICKER_BLOCKED_BY_ANNOUNCEMENT_ONLY",
comment: "Message indicating that only administrators can send message to an announcement-only group.")
let toastText = String(format: toastFormat, NSNumber(value: kMaxPickerSelection))
showToast(message: toastText)
}
private func didDeselect(conversation: ConversationItem) {
AssertIsOnMainThread()
selection.remove(conversation)
updateUIForCurrentSelection(animated: true)
}
private func updateUIForCurrentSelection(animated: Bool) {
let conversations = selection.conversations
let labelText = conversations.map { $0.title }.joined(separator: ", ")
footerView.setNamesText(labelText, animated: animated)
}
private func showTooManySelectedToast() {
Logger.info("")
let toastFormat = NSLocalizedString("CONVERSATION_PICKER_CAN_SELECT_NO_MORE_CONVERSATIONS",
comment: "Momentarily shown to the user when attempting to select more conversations than is allowed. Embeds {{max number of conversations}} that can be selected.")
let toastText = String(format: toastFormat, NSNumber(value: kMaxPickerSelection))
showToast(message: toastText)
}
private func showToast(message: String) {
Logger.info("")
let toastController = ToastController(text: message)
let bottomInset = (view.bounds.height - tableView.frame.height)
let kToastInset: CGFloat = bottomInset + 10
toastController.presentToastView(fromBottomOfView: view, inset: kToastInset)
}
}
// MARK: -
extension ConversationPickerViewController: UISearchBarDelegate {
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
firstly {
buildSearchResults(searchText: searchText)
}.then { [weak self] searchResults -> Promise<ConversationCollection> in
guard let self = self else {
throw PromiseError.cancelled
}
return self.buildConversationCollection(searchResults: searchResults)
}.done(on: .main) { [weak self] conversationCollection in
guard let self = self else { return }
self.conversationCollection = conversationCollection
}.catch { error in
owsFailDebug("Error: \(error)")
}
}
public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.setShowsCancelButton(true, animated: true)
pickerDelegate?.conversationPickerSearchBarActiveDidChange(self)
restoreSelection()
}
public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
searchBar.setShowsCancelButton(false, animated: true)
pickerDelegate?.conversationPickerSearchBarActiveDidChange(self)
restoreSelection()
}
public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = nil
searchBar.resignFirstResponder()
if shouldHideSearchBarIfCancelled {
self.shouldShowSearchBar = false
}
conversationCollection = buildConversationCollection()
pickerDelegate?.conversationPickerSearchBarActiveDidChange(self)
}
public func resetSearchBarText() {
guard nil != searchBar.text?.nilIfEmpty else {
return
}
searchBar.text = nil
conversationCollection = buildConversationCollection()
}
public var isSearchBarActive: Bool {
searchBar.isFirstResponder
}
}
// MARK: -
extension ConversationPickerViewController: ApprovalFooterDelegate {
public func approvalFooterDelegateDidRequestProceed(_ approvalFooterView: ApprovalFooterView) {
guard let pickerDelegate = pickerDelegate else {
owsFailDebug("Missing delegate.")
return
}
let conversations = selection.conversations
guard conversations.count > 0 else {
Logger.warn("No conversations selected.")
return
}
pickerDelegate.conversationPickerDidCompleteSelection(self)
}
public func approvalMode(_ approvalFooterView: ApprovalFooterView) -> ApprovalMode {
return approvalMode
}
public func approvalFooterDidBeginEditingText() {
AssertIsOnMainThread()
pickerDelegate?.conversationPickerDidBeginEditingText()
shouldShowSearchBar = false
}
}
// MARK: -
extension ConversationPickerViewController {
private struct Strings {
static let title = NSLocalizedString("CONVERSATION_PICKER_TITLE", comment: "navbar header")
static let recentsSection = NSLocalizedString("CONVERSATION_PICKER_SECTION_RECENTS", comment: "table section header for section containing recent conversations")
static let signalContactsSection = NSLocalizedString("CONVERSATION_PICKER_SECTION_SIGNAL_CONTACTS", comment: "table section header for section containing contacts")
static let groupsSection = NSLocalizedString("CONVERSATION_PICKER_SECTION_GROUPS", comment: "table section header for section containing groups")
}
}
// MARK: - ConversationPickerCell
private class ConversationPickerCell: ContactTableViewCell {
open override class var reuseIdentifier: String { "ConversationPickerCell" }
// MARK: - UITableViewCell
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
applySelection()
}
private func applySelection() {
selectedBadgeView.isHidden = !self.isSelected
unselectedBadgeView.isHidden = self.isSelected
}
// MARK: - ContactTableViewCell
public func configure(conversationItem: ConversationItem, transaction: SDSAnyReadTransaction) {
let content: ConversationContent
switch conversationItem.messageRecipient {
case .contact(let address):
content = ConversationContent.forAddress(address, transaction: transaction)
case .group(let groupThreadId):
guard let groupThread = TSGroupThread.anyFetchGroupThread(
uniqueId: groupThreadId,
transaction: transaction
) else {
owsFailDebug("Failed to find group thread")
return
}
content = ConversationContent.forThread(groupThread)
}
let configuration = ContactCellConfiguration(content: content,
localUserDisplayMode: .noteToSelf)
if conversationItem.isBlocked {
configuration.accessoryMessage = MessageStrings.conversationIsBlocked
} else {
configuration.accessoryView = buildAccessoryView(disappearingMessagesConfig: conversationItem.disappearingMessagesConfig)
}
super.configure(configuration: configuration, transaction: transaction)
// Apply theme.
unselectedBadgeView.layer.borderColor = Theme.primaryIconColor.cgColor
selectionStyle = .none
applySelection()
}
// MARK: - Subviews
static let selectedBadgeImage = #imageLiteral(resourceName: "image_editor_checkmark_full").withRenderingMode(.alwaysTemplate)
let selectionBadgeSize = CGSize(square: 20)
lazy var selectionView: UIView = {
let container = UIView()
container.layoutMargins = .zero
container.autoSetDimensions(to: selectionBadgeSize)
container.addSubview(unselectedBadgeView)
unselectedBadgeView.autoPinEdgesToSuperviewMargins()
container.addSubview(selectedBadgeView)
selectedBadgeView.autoPinEdgesToSuperviewMargins()
return container
}()
func buildAccessoryView(disappearingMessagesConfig: OWSDisappearingMessagesConfiguration?) -> ContactCellAccessoryView {
selectionView.removeFromSuperview()
let selectionWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(selectionView)
guard let disappearingMessagesConfig = disappearingMessagesConfig,
disappearingMessagesConfig.isEnabled else {
return ContactCellAccessoryView(accessoryView: selectionWrapper,
size: selectionBadgeSize)
}
let timerView = DisappearingTimerConfigurationView(durationSeconds: disappearingMessagesConfig.durationSeconds)
timerView.tintColor = Theme.middleGrayColor
let timerSize = CGSize(square: 44)
let stackView = ManualStackView(name: "stackView")
let stackConfig = OWSStackView.Config(axis: .horizontal,
alignment: .center,
spacing: 0,
layoutMargins: .zero)
let stackMeasurement = stackView.configure(config: stackConfig,
subviews: [timerView, selectionWrapper],
subviewInfos: [
timerSize.asManualSubviewInfo,
selectionBadgeSize.asManualSubviewInfo
])
let stackSize = stackMeasurement.measuredSize
return ContactCellAccessoryView(accessoryView: stackView, size: stackSize)
}
lazy var unselectedBadgeView: UIView = {
let circleView = CircleView()
circleView.autoSetDimensions(to: selectionBadgeSize)
circleView.layer.borderWidth = 1.0
circleView.layer.borderColor = Theme.primaryIconColor.cgColor
return circleView
}()
lazy var selectedBadgeView: UIView = {
let imageView = UIImageView()
imageView.autoSetDimensions(to: selectionBadgeSize)
imageView.image = ConversationPickerCell.selectedBadgeImage
imageView.tintColor = .ows_accentBlue
return imageView
}()
}
// MARK: -
extension ConversationPickerViewController: ConversationPickerSelectionDelegate {
func conversationPickerSelectionDidAdd() {
AssertIsOnMainThread()
pickerDelegate?.conversationPickerSelectionDidChange(self)
// Clear the search text, if any.
resetSearchBarText()
}
func conversationPickerSelectionDidRemove() {
AssertIsOnMainThread()
pickerDelegate?.conversationPickerSelectionDidChange(self)
}
}
// MARK: -
protocol ConversationPickerSelectionDelegate: AnyObject {
func conversationPickerSelectionDidAdd()
func conversationPickerSelectionDidRemove()
}
// MARK: -
public class ConversationPickerSelection {
fileprivate weak var delegate: ConversationPickerSelectionDelegate?
public private(set) var conversations: [ConversationItem] = []
public required init() {}
public func add(_ conversation: ConversationItem) {
conversations.append(conversation)
delegate?.conversationPickerSelectionDidAdd()
}
public func remove(_ conversation: ConversationItem) {
conversations.removeAll { $0.messageRecipient == conversation.messageRecipient }
delegate?.conversationPickerSelectionDidRemove()
}
public func isSelected(conversation: ConversationItem) -> Bool {
!conversations.filter { $0.messageRecipient == conversation.messageRecipient }.isEmpty
}
}
// MARK: -
private struct ConversationCollection {
static let empty: ConversationCollection = ConversationCollection(contactConversations: [],
recentConversations: [],
groupConversations: [],
isSearchResults: false)
let contactConversations: [ConversationItem]
let recentConversations: [ConversationItem]
let groupConversations: [ConversationItem]
let isSearchResults: Bool
var allConversations: [ConversationItem] {
recentConversations + contactConversations + groupConversations
}
private func conversations(section: Section) -> [ConversationItem] {
switch section {
case .recents:
return recentConversations
case .signalContacts:
return contactConversations
case .groups:
return groupConversations
}
}
private enum Section: Int, CaseIterable {
case recents, signalContacts, groups
}
fileprivate func indexPath(conversation: ConversationItem) -> IndexPath? {
switch conversation.messageRecipient {
case .contact:
if let row = (recentConversations.map { $0.messageRecipient }).firstIndex(of: conversation.messageRecipient) {
return IndexPath(row: row, section: Section.recents.rawValue)
} else if let row = (contactConversations.map { $0.messageRecipient }).firstIndex(of: conversation.messageRecipient) {
return IndexPath(row: row, section: Section.signalContacts.rawValue)
} else {
return nil
}
case .group:
if let row = (recentConversations.map { $0.messageRecipient }).firstIndex(of: conversation.messageRecipient) {
return IndexPath(row: row, section: Section.recents.rawValue)
} else if let row = (groupConversations.map { $0.messageRecipient }).firstIndex(of: conversation.messageRecipient) {
return IndexPath(row: row, section: Section.groups.rawValue)
} else {
return nil
}
}
}
fileprivate func conversation(for indexPath: IndexPath) -> ConversationItem? {
guard let section = Section(rawValue: indexPath.section) else {
owsFailDebug("section was unexpectedly nil")
return nil
}
return conversations(section: section)[indexPath.row]
}
fileprivate func conversation(for thread: TSThread) -> ConversationItem? {
allConversations.first { item in
if let thread = thread as? TSGroupThread, case .group(let otherThreadId) = item.messageRecipient {
return thread.uniqueId == otherThreadId
} else if let thread = thread as? TSContactThread, case .contact(let otherAddress) = item.messageRecipient {
return thread.contactAddress == otherAddress
} else {
return false
}
}
}
}