TM-SGNL-iOS/SignalUI/RecipientPickers/TSGroupThread+ViewModel.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

88 lines
3.5 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import SignalServiceKit
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: SSKEnvironment.shared.contactManagerRef),
transaction: SDSAnyReadTransaction
) -> [String] {
let tx = transaction.asV2Read
let config: DisplayName.ComparableValue.Config = .current()
let members = groupMembership.fullMembers.compactMap { address -> (
comparableName: ComparableDisplayName,
matchedDisplayName: String?
)? in
guard !address.isLocalAddress else {
return nil
}
guard includingBlocked || !SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(address, transaction: transaction) else {
return nil
}
let displayName = nameResolver.displayName(for: address, tx: tx)
let comparableName = ComparableDisplayName(address: address, displayName: displayName, config: config)
var wrappedDisplayNameMatch: String?
if let searchText {
wrappedDisplayNameMatch = wrapIfMatch(
searchText: searchText,
displayName: comparableName.resolvedValue(useShortNameIfAvailable: useShortNameIfAvailable)
)
}
return (comparableName: comparableName, 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
}
return lhs.comparableName < rhs.comparableName
}
return sortedMembers.lazy.prefix(limit).map {
if let matchedDisplayName = $0.matchedDisplayName {
return matchedDisplayName
}
return $0.comparableName.resolvedValue(useShortNameIfAvailable: useShortNameIfAvailable)
}
}
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: "<\(FullTextSearchIndexer.matchTag)>\(displayName[matchRange])</\(FullTextSearchIndexer.matchTag)>"
)
}
}