mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:45:18 +00:00
* Fix lock glyph alignment, privacy-caption band, and empty-state wrapping - Message-row locks: align to first text baseline instead of a hardcoded top padding that left the lock ~4pt below the line's visual center - Header/caption locks: 1pt optical lift (lock.fill ink is bottom-heavy; geometric centering reads low); seal badge stays untouched - DM privacy caption: sit on the themed surface like the rest of the bottom chrome instead of painting its own orange band - Empty-state lines: non-breaking spaces so the closing * can't orphan and 'bitchat/ for help' can't break right after the slash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Unify sheet close buttons, widen tiny tap targets, handle long nicknames - New SheetCloseButton component: one glyph size/weight (13 semibold), 32pt visual box, 44pt hit target; adopted by all 7 sheets (sizes had drifted across 12/13/14pt, two had no frame at all) - Favorite star buttons get real tap targets (peer list + DM header) - DM header nickname: single line with middle truncation instead of wrapping into the fixed-height header; peer-list names truncate tail - Geohash people rows: leading glyph 12 -> 10 to match mesh rows - Sidebar lock glyphs get the same optical lift as the DM header Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make channel switching, voice notes, and header actions work under VoiceOver - Channel rows in the location sheet are now single activatable buttons (label + selected trait + switch hint) with the bookmark toggle mirrored as a named accessibility action; bookmark buttons labeled - Voice-note mic: press-and-hold drag gestures can't be activated by VoiceOver, so the default accessibility action now toggles start/stop-and-send; announces 'recording' state; localized labels - Attachment button: camera (long-press) path exposed as a named action; labels localized instead of hardcoded English - People-count button announces connected vs no-one-reachable (was color-only); verification QR button gains a spoken name (.help is only a hint on iOS); bitchat/ logo exposes its tap-for-app-info as a button (panic triple-tap stays undiscoverable on purpose) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Theme-correctness sweep: palette colors everywhere, AX-size header growth - Fingerprint/verification sheet cards: palette-tinted boxes instead of fixed gray bands that ignored matrix green and occluded glass - Voice-note card: palette background (translucent) instead of opaque white/black; waveform + payment chips + image placeholder follow suit - .secondary/.primary/Color.blue swapped for palette.secondary/primary/ accentBlue across location sheets, people sheets, message captions, and the header count (system gray read wrong under matrix green) - Autocomplete/command rows: dropped the uniform gray wash that dulled the themed overlay panel - 'tap to reveal' caption follows the theme font instead of hardcoding monospaced - Headers use minHeight so two-line accessibility text sizes grow the bar instead of clipping inside it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix main header expanding to fill the screen The header bar's fixed height was load-bearing: its children fill the bar with .frame(maxHeight: .infinity) tap targets, so switching to an open-ended minHeight let the header expand to swallow all available vertical space, centering the title mid-screen and crushing the timeline into the composer. Restore the fixed height — headerHeight is a @ScaledMetric, so it already grows with Dynamic Type. Reproduced and verified both layouts with an offscreen render harness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * DM header: floating glass panel instead of muddy orange wash under glass Orange at 14% over the backdrop gradient reads as a gray-beige band, not a privacy signature. Under liquid glass the DM header now uses the same floating chrome panel as the main header; the private signature is already carried by the orange lock, caption, and composer accents. Matrix keeps its orange wash over the opaque themed surface, unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
167 lines
8.7 KiB
Swift
167 lines
8.7 KiB
Swift
import SwiftUI
|
|
|
|
struct GeohashPeopleList: View {
|
|
@EnvironmentObject private var peerListModel: PeerListModel
|
|
@ThemedPalette private var palette
|
|
let onTapPerson: () -> Void
|
|
@Environment(\.colorScheme) var colorScheme
|
|
@State private var orderedIDs: [String] = []
|
|
|
|
private enum Strings {
|
|
static let noneNearby: LocalizedStringKey = "geohash_people.none_nearby"
|
|
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 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")
|
|
static let blockText = String(localized: "geohash_people.action.block", comment: "Context menu action to block a person")
|
|
static let teleported = String(localized: "geohash_people.state.teleported", comment: "State label for someone who joined the location channel from elsewhere")
|
|
static let nearby = String(localized: "geohash_people.state.nearby", comment: "State label for someone physically in the location channel's area")
|
|
static let blockedState = String(localized: "mesh_peers.state.blocked", comment: "State label for a blocked peer")
|
|
static let youState = String(localized: "geohash_people.state.you", comment: "State label marking your own row in the people list")
|
|
static let openDMHint = String(localized: "mesh_peers.accessibility.open_dm_hint", comment: "Accessibility hint on a peer row explaining activation opens a private chat")
|
|
}
|
|
|
|
var body: some View {
|
|
if peerListModel.geohashPeople.isEmpty {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(Strings.noneNearby)
|
|
.bitchatFont(size: 14)
|
|
.foregroundColor(palette.secondary)
|
|
.padding(.horizontal)
|
|
.padding(.top, 12)
|
|
}
|
|
} else {
|
|
let people = peerListModel.geohashPeople
|
|
let currentIDs = people.map(\.id)
|
|
|
|
let displayIDs = orderedIDs.filter { currentIDs.contains($0) } + currentIDs.filter { !orderedIDs.contains($0) }
|
|
let nonTele = displayIDs.filter { id in
|
|
!(people.first(where: { $0.id == id })?.isTeleported ?? false)
|
|
}
|
|
let tele = displayIDs.filter { id in
|
|
people.first(where: { $0.id == id })?.isTeleported ?? false
|
|
}
|
|
let finalOrder: [String] = nonTele + tele
|
|
let firstID = finalOrder.first
|
|
let personByID = Dictionary(uniqueKeysWithValues: people.map { ($0.id, $0) })
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
ForEach(finalOrder.filter { personByID[$0] != nil }, id: \.self) { pid in
|
|
let person = personByID[pid]!
|
|
HStack(spacing: 4) {
|
|
let icon = person.isTeleported ? "face.dashed" : "mappin.and.ellipse"
|
|
let assignedColor = peerListModel.colorForGeohashPerson(id: person.id, isDark: colorScheme == .dark)
|
|
let rowColor: Color = person.isMe ? .orange : assignedColor
|
|
Image(systemName: icon)
|
|
// Size 10 to match the mesh rows' leading glyphs —
|
|
// both lists share the sidebar.
|
|
.font(.bitchatSystem(size: 10))
|
|
.foregroundColor(rowColor)
|
|
.help(person.isTeleported ? Strings.teleported : Strings.nearby)
|
|
|
|
let (base, suffix) = person.displayName.splitSuffix()
|
|
HStack(spacing: 0) {
|
|
Text(base)
|
|
.bitchatFont(size: 14)
|
|
.fontWeight(person.isMe ? .bold : .regular)
|
|
.foregroundColor(rowColor)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
if !suffix.isEmpty {
|
|
let suffixColor = person.isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
|
|
Text(suffix)
|
|
.bitchatFont(size: 14)
|
|
.foregroundColor(suffixColor)
|
|
}
|
|
if person.isMe {
|
|
Text(Strings.youSuffix)
|
|
.bitchatFont(size: 14)
|
|
.foregroundColor(rowColor)
|
|
}
|
|
}
|
|
if person.isBlocked {
|
|
Image(systemName: "nosign")
|
|
.font(.bitchatSystem(size: 10))
|
|
.foregroundColor(.red)
|
|
.help(Strings.blockedTooltip)
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 4)
|
|
.padding(.top, person.id == firstID ? 10 : 0)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
if !person.isMe {
|
|
peerListModel.openGeohashDirectMessage(with: person.id)
|
|
onTapPerson()
|
|
}
|
|
}
|
|
.contextMenu {
|
|
if person.isMe {
|
|
EmptyView()
|
|
} else {
|
|
if person.isBlocked {
|
|
Button(Strings.unblock) {
|
|
peerListModel.unblockGeohashUser(
|
|
pubkeyHexLowercased: person.id,
|
|
displayName: person.displayName
|
|
)
|
|
}
|
|
} else {
|
|
Button(Strings.block) {
|
|
peerListModel.blockGeohashUser(
|
|
pubkeyHexLowercased: person.id,
|
|
displayName: person.displayName
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel(accessibilityDescription(for: person))
|
|
.accessibilityAddTraits(person.isMe ? [] : .isButton)
|
|
.accessibilityHint(person.isMe ? "" : Strings.openDMHint)
|
|
.accessibilityActions {
|
|
if !person.isMe {
|
|
Button(person.isBlocked ? Strings.unblockText : Strings.blockText) {
|
|
if person.isBlocked {
|
|
peerListModel.unblockGeohashUser(
|
|
pubkeyHexLowercased: person.id,
|
|
displayName: person.displayName
|
|
)
|
|
} else {
|
|
peerListModel.blockGeohashUser(
|
|
pubkeyHexLowercased: person.id,
|
|
displayName: person.displayName
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Seed and update order outside result builder
|
|
.onAppear {
|
|
orderedIDs = currentIDs
|
|
}
|
|
.onChange(of: currentIDs) { ids in
|
|
var newOrder = orderedIDs
|
|
newOrder.removeAll { !ids.contains($0) }
|
|
for id in ids where !newOrder.contains(id) { newOrder.append(id) }
|
|
if newOrder != orderedIDs { orderedIDs = newOrder }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One spoken sentence per row: name, presence type, and block state.
|
|
private func accessibilityDescription(for person: GeohashPersonRow) -> String {
|
|
var parts: [String] = [person.displayName]
|
|
if person.isMe { parts.append(Strings.youState) }
|
|
parts.append(person.isTeleported ? Strings.teleported : Strings.nearby)
|
|
if person.isBlocked { parts.append(Strings.blockedState) }
|
|
return parts.joined(separator: ", ")
|
|
}
|
|
}
|