Signal-iOS/SignalUI/ViewModels/TSGroupThread+ViewModel.swift
2023-04-10 19:31:46 -07:00

103 lines
3.9 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalMessaging
public extension TSGroupThread {
/// Returns a list of up to `limit` names of group members.
///
/// The list will not contain the local user. If `includingBlocked` is
/// `false`, it will also not contain any users that have been blocked by
/// the local user.
///
/// The name returned is computed by `getDisplayName`, but sorting is always
/// done using `ContactsManager.comparableName(for:transaction:)`. Phone
/// numbers are sorted to the end of the list.
///
/// If `searchText` is provided, members will be sorted to the front of the
/// list if their display names (as returned by `getDisplayName`) contain
/// the string. The names will also have the matching substring bracketed as
/// `<match>substring</match>`, similar to the results of
/// FullTextSearchFinder.
func sortedMemberNames(
searchText: String? = nil,
includingBlocked: Bool,
limit: Int = .max,
useShortNameIfAvailable: Bool = false,
nameResolver: NameResolver = NameResolverImpl(contactsManager: TSGroupThread.contactsManager),
transaction: SDSAnyReadTransaction
) -> [String] {
let transactionV2 = transaction.asV2Read
let members = groupMembership.fullMembers.compactMap { address -> (
address: SignalServiceAddress,
comparableName: String,
matchedDisplayName: String?
)? in
guard !address.isLocalAddress else {
return nil
}
guard includingBlocked || !blockingManager.isAddressBlocked(address, transaction: transaction) else {
return nil
}
var wrappedDisplayNameMatch: String?
if let searchText {
wrappedDisplayNameMatch = wrapIfMatch(
searchText: searchText,
displayName: nameResolver.displayName(
for: address,
useShortNameIfAvailable: useShortNameIfAvailable,
transaction: transactionV2
)
)
}
return (
address: address,
comparableName: nameResolver.comparableName(for: address, transaction: transactionV2),
matchedDisplayName: wrappedDisplayNameMatch
)
}
let sortedMembers = members.sorted { lhs, rhs in
// Bubble matched members to the top
if (rhs.matchedDisplayName != nil) != (lhs.matchedDisplayName != nil) {
return lhs.matchedDisplayName != nil
}
// Sort numbers to the end of the list
if lhs.comparableName.hasPrefix("+") != rhs.comparableName.hasPrefix("+") {
return !lhs.comparableName.hasPrefix("+")
}
// Otherwise, sort by comparable name
return lhs.comparableName.caseInsensitiveCompare(rhs.comparableName) == .orderedAscending
}
return sortedMembers.lazy.prefix(limit).map {
if let matchedDisplayName = $0.matchedDisplayName {
return matchedDisplayName
}
return nameResolver.displayName(
for: $0.address,
useShortNameIfAvailable: useShortNameIfAvailable,
transaction: transactionV2
)
}
}
private func wrapIfMatch(searchText: String, displayName: String) -> String? {
guard
let matchRange = displayName.range(of: searchText, options: [.caseInsensitive, .diacriticInsensitive])
else {
return nil
}
return displayName.replacingCharacters(
in: matchRange,
with: "<\(FullTextSearchFinder.matchTag)>\(displayName[matchRange])</\(FullTextSearchFinder.matchTag)>"
)
}
}