feat: filter mesh, geohash, bridge, and group rows by name

Each people-sheet section accepts an optional nameFilter so a shared
search field can narrow nicknames without reshaping PeerListModel.
This commit is contained in:
Taksh 2026-08-01 14:08:57 +03:00
parent 5b5adf2e2e
commit addcc3e701
4 changed files with 41 additions and 7 deletions

View File

@ -39,6 +39,8 @@ struct PeopleSectionHeader: View {
struct BridgePeopleList: View {
@ObservedObject private var bridgeService = BridgeService.shared
@ThemedPalette private var palette
/// People-sheet search query; empty means show every row.
var nameFilter: String = ""
private enum Strings {
static let sectionTitle = String(localized: "bridge_people.section_title", defaultValue: "across the bridge", comment: "Section header in the people sheet for participants reachable via the mesh bridge")
@ -50,7 +52,10 @@ struct BridgePeopleList: View {
// (a serving neighbor's carriers) even while this device's own
// bridge is off — whoever is visible in the timeline belongs in the
// sheet.
if !bridgeService.bridgedParticipants.isEmpty {
let people = bridgeService.bridgedParticipants.filter {
PeopleNameFilter.matches($0.displayName, query: nameFilter)
}
if !people.isEmpty {
VStack(alignment: .leading, spacing: 0) {
PeopleSectionHeader(
icon: "network",
@ -58,7 +63,7 @@ struct BridgePeopleList: View {
title: Strings.sectionTitle
)
ForEach(bridgeService.bridgedParticipants) { person in
ForEach(people) { person in
HStack(spacing: 4) {
Text(person.displayName)
.bitchatFont(size: 14)

View File

@ -4,13 +4,16 @@ struct GeohashPeopleList: View {
@EnvironmentObject private var peerListModel: PeerListModel
@ThemedPalette private var palette
let onTapPerson: () -> Void
/// People-sheet search query; empty means show every row.
var nameFilter: String = ""
@Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = []
private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let noMatches: LocalizedStringKey = "content.people.search.no_matches"
static let youSuffix: LocalizedStringKey = "geohash_people.you_suffix"
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to users blocked in geohash channels")
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to people blocked in geohash channels")
static let unblock: LocalizedStringKey = "geohash_people.action.unblock"
static let block: LocalizedStringKey = "geohash_people.action.block"
static let unblockText = String(localized: "geohash_people.action.unblock", comment: "Context menu action to unblock a person")
@ -32,7 +35,19 @@ struct GeohashPeopleList: View {
.padding(.top, 12)
}
} else {
let people = peerListModel.geohashPeople
let people = peerListModel.geohashPeople.filter {
PeopleNameFilter.matches($0.displayName, query: nameFilter)
}
let filterActive = !nameFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if people.isEmpty && filterActive {
VStack(alignment: .leading, spacing: 0) {
Text(Strings.noMatches)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
.padding(.horizontal)
.padding(.top, 12)
}
} else {
let currentIDs = people.map(\.id)
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
@ -152,6 +167,7 @@ struct GeohashPeopleList: View {
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
if newOrder != orderedIDs { orderedIDs = newOrder }
}
}
}
}

View File

@ -8,6 +8,8 @@ struct GroupChatList: View {
let groups: [GroupChatRow]
let onTapGroup: (PeerID) -> Void
/// People-sheet search query; empty means show every row.
var nameFilter: String = ""
private enum Strings {
static let header = String(localized: "groups.section.header", comment: "Section header above the private groups list")
@ -19,7 +21,8 @@ struct GroupChatList: View {
}
var body: some View {
if !groups.isEmpty {
let visible = groups.filter { PeopleNameFilter.matches($0.name, query: nameFilter) }
if !visible.isEmpty {
VStack(alignment: .leading, spacing: 0) {
// Same glyph+label header shape as #mesh / across the bridge.
PeopleSectionHeader(
@ -28,7 +31,7 @@ struct GroupChatList: View {
title: Strings.header
)
ForEach(groups) { group in
ForEach(visible) { group in
HStack(spacing: 4) {
Text("#\(group.name)")
.bitchatFont(size: 14)

View File

@ -10,12 +10,15 @@ struct MeshPeerList: View {
/// Optional so existing call sites (and previews/tests) keep compiling;
/// when absent the block/unblock context-menu entry is hidden.
var onToggleBlock: ((MeshPeerRow) -> Void)? = nil
/// People-sheet search query; empty means show every row.
var nameFilter: String = ""
@Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = []
private enum Strings {
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
static let noMatches: LocalizedStringKey = "content.people.search.no_matches"
static let blockedTooltip = String(localized: "geohash_people.tooltip.blocked", comment: "Tooltip shown next to a blocked peer indicator")
static let newMessagesTooltip = String(localized: "mesh_peers.tooltip.new_messages", comment: "Tooltip for the unread messages indicator")
static let connected = String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator")
@ -41,7 +44,8 @@ struct MeshPeerList: View {
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
let peers: [MeshPeerRow] = displayIDs.compactMap { id in
peerListModel.meshRows.first(where: { $0.id == id })
}
}.filter { PeopleNameFilter.matches($0.displayName, query: nameFilter) }
let filterActive = !nameFilter.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if peerListModel.meshRows.isEmpty {
// Match the section's row rhythm (same size, indent, and vertical
@ -52,6 +56,12 @@ struct MeshPeerList: View {
.foregroundColor(palette.secondary)
.padding(.horizontal)
.padding(.vertical, 4)
} else if peers.isEmpty && filterActive {
Text(Strings.noMatches)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
.padding(.horizontal)
.padding(.vertical, 4)
} else {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<peers.count, id: \.self) { idx in