Support Dynamic Type across chat surfaces (#664)

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-09-23 19:56:16 +02:00
committed by GitHub
co-authored by jack
parent 10c0391eaf
commit c49a1b264e
11 changed files with 245 additions and 174 deletions
+37
View File
@@ -0,0 +1,37 @@
import SwiftUI
/// Provides Dynamic Type aware font helpers that map existing fixed sizes onto
/// preferred text styles so the UI scales with user accessibility settings.
extension Font {
static func bitchatSystem(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> Font {
let style = Font.TextStyle.bitchatPreferredStyle(for: size)
var font = Font.system(style, design: design)
if weight != .regular {
font = font.weight(weight)
}
return font
}
}
private extension Font.TextStyle {
static func bitchatPreferredStyle(for size: CGFloat) -> Font.TextStyle {
switch size {
case ..<11.5:
return .caption2
case ..<13.0:
return .caption
case ..<14.0:
return .footnote
case ..<16.5:
return .body
case ..<19.0:
return .title3
case ..<23.0:
return .title2
case ..<30.0:
return .title
default:
return .largeTitle
}
}
}
+26 -26
View File
@@ -3323,7 +3323,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
senderStyle.foregroundColor = baseColor
// Bold the user's own nickname
let fontWeight: Font.Weight = isSelf ? .bold : .medium
senderStyle.font = .system(size: 14, weight: fontWeight, design: .monospaced)
senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: .monospaced)
// Make sender clickable: encode senderPeerID into a custom URL
if let spid = message.senderPeerID, let url = URL(string: "bitchat://user/\(spid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? spid)") {
senderStyle.link = url
@@ -3358,8 +3358,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
var plainStyle = AttributeContainer()
plainStyle.foregroundColor = baseColor
plainStyle.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
result.append(AttributedString(content).mergingAttributes(plainStyle))
} else {
// Reuse compiled regexes and detector
@@ -3455,8 +3455,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
var beforeStyle = AttributeContainer()
beforeStyle.foregroundColor = baseColor
beforeStyle.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
if isMentioned {
beforeStyle.font = beforeStyle.font?.bold()
}
@@ -3486,7 +3486,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return false
}()
var mentionStyle = AttributeContainer()
mentionStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
mentionStyle.font = .bitchatSystem(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
let mentionColor: Color = isMentionToMe ? .orange : baseColor
mentionStyle.foregroundColor = mentionColor
// Emit '@'
@@ -3530,8 +3530,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
}()
var tagStyle = AttributeContainer()
tagStyle.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
tagStyle.foregroundColor = baseColor
if isGeohash && !attachedToMention && standalone, let url = URL(string: "bitchat://geohash/\(token)") {
tagStyle.link = url
@@ -3544,21 +3544,21 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
var spacer = AttributeContainer()
spacer.foregroundColor = baseColor
spacer.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
result.append(AttributedString(" ").mergingAttributes(spacer))
} else if type == "lightning" || type == "bolt11" || type == "lnurl" {
// Skip inline invoice/link; a styled chip is rendered below the message
var spacer = AttributeContainer()
spacer.foregroundColor = baseColor
spacer.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
result.append(AttributedString(" ").mergingAttributes(spacer))
} else {
// Keep URL styling and make it tappable via .link attribute
var matchStyle = AttributeContainer()
matchStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
matchStyle.font = .bitchatSystem(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
if type == "url" {
matchStyle.foregroundColor = isSelf ? .orange : .blue
matchStyle.underlineStyle = .single
@@ -3582,8 +3582,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
var remainingStyle = AttributeContainer()
remainingStyle.foregroundColor = baseColor
remainingStyle.font = isSelf
? .system(size: 14, weight: .bold, design: .monospaced)
: .system(size: 14, design: .monospaced)
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
: .bitchatSystem(size: 14, design: .monospaced)
if isMentioned {
remainingStyle.font = remainingStyle.font?.bold()
}
@@ -3595,21 +3595,21 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
var timestampStyle = AttributeContainer()
timestampStyle.foregroundColor = Color.gray.opacity(0.7)
timestampStyle.font = .system(size: 10, design: .monospaced)
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
result.append(timestamp.mergingAttributes(timestampStyle))
} else {
// System message
var contentStyle = AttributeContainer()
contentStyle.foregroundColor = Color.gray
let content = AttributedString("* \(message.content) *")
contentStyle.font = .system(size: 12, design: .monospaced).italic()
contentStyle.font = .bitchatSystem(size: 12, design: .monospaced).italic()
result.append(content.mergingAttributes(contentStyle))
// Add timestamp at the end for system messages too
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
var timestampStyle = AttributeContainer()
timestampStyle.foregroundColor = Color.gray.opacity(0.5)
timestampStyle.font = .system(size: 10, design: .monospaced)
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
result.append(timestamp.mergingAttributes(timestampStyle))
}
@@ -3629,14 +3629,14 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let content = AttributedString("* \(message.content) *")
var contentStyle = AttributeContainer()
contentStyle.foregroundColor = Color.gray
contentStyle.font = .system(size: 12, design: .monospaced).italic()
contentStyle.font = .bitchatSystem(size: 12, design: .monospaced).italic()
result.append(content.mergingAttributes(contentStyle))
// Add timestamp at the end for system messages
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
var timestampStyle = AttributeContainer()
timestampStyle.foregroundColor = Color.gray.opacity(0.5)
timestampStyle.font = .system(size: 10, design: .monospaced)
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
result.append(timestamp.mergingAttributes(timestampStyle))
} else {
let sender = AttributedString("<@\(message.sender)> ")
@@ -3646,7 +3646,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
senderStyle.foregroundColor = primaryColor
// Bold the user's own nickname
let fontWeight: Font.Weight = message.sender == nickname ? .bold : .medium
senderStyle.font = .system(size: 12, weight: fontWeight, design: .monospaced)
senderStyle.font = .bitchatSystem(size: 12, weight: fontWeight, design: .monospaced)
result.append(sender.mergingAttributes(senderStyle))
@@ -3670,7 +3670,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
if !beforeText.isEmpty {
var normalStyle = AttributeContainer()
normalStyle.font = .system(size: 14, design: .monospaced)
normalStyle.font = .bitchatSystem(size: 14, design: .monospaced)
normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
}
@@ -3679,7 +3679,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Add the mention with highlight
let mentionText = String(contentText[range])
var mentionStyle = AttributeContainer()
mentionStyle.font = .system(size: 14, weight: .semibold, design: .monospaced)
mentionStyle.font = .bitchatSystem(size: 14, weight: .semibold, design: .monospaced)
mentionStyle.foregroundColor = Color.orange
processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle))
@@ -3691,7 +3691,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
if lastEndIndex < contentText.endIndex {
let remainingText = String(contentText[lastEndIndex...])
var normalStyle = AttributeContainer()
normalStyle.font = .system(size: 14, design: .monospaced)
normalStyle.font = .bitchatSystem(size: 14, design: .monospaced)
normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(remainingText).mergingAttributes(normalStyle))
}
@@ -3702,7 +3702,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let relay = AttributedString(" (via \(originalSender))")
var relayStyle = AttributeContainer()
relayStyle.foregroundColor = primaryColor.opacity(0.7)
relayStyle.font = .system(size: 11, design: .monospaced)
relayStyle.font = .bitchatSystem(size: 11, design: .monospaced)
result.append(relay.mergingAttributes(relayStyle))
}
@@ -3710,7 +3710,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
var timestampStyle = AttributeContainer()
timestampStyle.foregroundColor = Color.gray.opacity(0.7)
timestampStyle.font = .system(size: 10, design: .monospaced)
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
result.append(timestamp.mergingAttributes(timestampStyle))
}
+20 -10
View File
@@ -88,7 +88,7 @@ struct AppInfoView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.frame(width: 32, height: 32)
}
@@ -106,11 +106,11 @@ struct AppInfoView: View {
// Header
VStack(alignment: .center, spacing: 8) {
Text(Strings.appName)
.font(.system(size: 32, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 32, weight: .bold, design: .monospaced))
.foregroundColor(textColor)
Text(Strings.tagline)
.font(.system(size: 16, design: .monospaced))
.font(.bitchatSystem(size: 16, design: .monospaced))
.foregroundColor(secondaryTextColor)
}
.frame(maxWidth: .infinity)
@@ -125,7 +125,7 @@ struct AppInfoView: View {
Text(instruction)
}
}
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(textColor)
}
@@ -181,7 +181,7 @@ struct AppInfoView: View {
.foregroundColor(Color.red)
Text(Strings.Warning.message)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(Color.red)
.fixedSize(horizontal: false, vertical: true)
}
@@ -211,7 +211,7 @@ struct SectionHeader: View {
var body: some View {
Text(title)
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(textColor)
.padding(.top, 8)
}
@@ -234,17 +234,17 @@ struct FeatureRow: View {
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: icon)
.font(.system(size: 20))
.font(.bitchatSystem(size: 20))
.foregroundColor(textColor)
.frame(width: 30)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
Text(description)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(secondaryTextColor)
.fixedSize(horizontal: false, vertical: true)
}
@@ -254,6 +254,16 @@ struct FeatureRow: View {
}
}
#Preview {
#Preview("Default") {
AppInfoView()
}
#Preview("Dynamic Type XXL") {
AppInfoView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
}
#Preview("Dynamic Type XS") {
AppInfoView()
.environment(\.sizeCategory, .extraSmall)
}
+55 -49
View File
@@ -30,6 +30,7 @@ struct ContentView: View {
@State private var textFieldSelection: NSRange? = nil
@FocusState private var isTextFieldFocused: Bool
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@State private var showPeerList = false
@State private var showSidebar = false
@State private var sidebarDragOffset: CGFloat = 0
@@ -53,6 +54,7 @@ struct ContentView: View {
@State private var showLocationNotes = false
@State private var notesGeohash: String? = nil
@State private var sheetNotesCount: Int = 0
@ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44
// Timer-based refresh removed; use LocationChannelManager live updates instead
// Window sizes for rendering (infinite scroll up)
@State private var windowCountPublic: Int = 300
@@ -63,14 +65,18 @@ struct ContentView: View {
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
private var headerLineLimit: Int? {
dynamicTypeSize.isAccessibilitySize ? 2 : 1
}
// MARK: - Body
@@ -328,7 +334,7 @@ struct ContentView: View {
if isExpanded { expandedMessageIDs.remove(message.id) }
else { expandedMessageIDs.insert(message.id) }
}
.font(.system(size: 11, weight: .medium, design: .monospaced))
.font(.bitchatSystem(size: 11, weight: .medium, design: .monospaced))
.foregroundColor(Color.blue)
.padding(.top, 4)
}
@@ -706,7 +712,7 @@ struct ContentView: View {
}) {
HStack {
Text(suggestion)
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(textColor)
.fontWeight(.medium)
Spacer()
@@ -764,14 +770,14 @@ struct ContentView: View {
HStack {
// Show all aliases together
Text(info.commands.joined(separator: ", "))
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(textColor)
.fontWeight(.medium)
// Show syntax if any
if let syntax = info.syntax {
Text(syntax)
.font(.system(size: 10, design: .monospaced))
.font(.bitchatSystem(size: 10, design: .monospaced))
.foregroundColor(secondaryTextColor.opacity(0.8))
}
@@ -779,7 +785,7 @@ struct ContentView: View {
// Show description
Text(info.description)
.font(.system(size: 10, design: .monospaced))
.font(.bitchatSystem(size: 10, design: .monospaced))
.foregroundColor(secondaryTextColor)
}
.padding(.horizontal, 12)
@@ -802,7 +808,7 @@ struct ContentView: View {
HStack(alignment: .center, spacing: 4) {
TextField("type a message...", text: $messageText)
.textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(textColor)
.focused($isTextFieldFocused)
.padding(.leading, 12)
@@ -877,7 +883,7 @@ struct ContentView: View {
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 20))
.font(.bitchatSystem(size: 20))
.foregroundColor(messageText.isEmpty ? Color.gray :
viewModel.selectedPrivateChatPeer != nil
? Color.orange : textColor)
@@ -918,20 +924,20 @@ struct ContentView: View {
// Header - match main toolbar height
HStack {
Text("PEOPLE")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(textColor)
Spacer()
// Show QR in mesh on all platforms
if case .mesh = locationManager.selectedChannel {
Button(action: { showVerifySheet = true }) {
Image(systemName: "qrcode")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
}
.buttonStyle(.plain)
.help("Verification: show my QR or scan a friend")
}
}
.frame(height: 44) // Match header height
.frame(height: headerHeight) // Match header height
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
@@ -1082,7 +1088,7 @@ struct ContentView: View {
private var mainHeaderView: some View {
HStack(spacing: 0) {
Text("bitchat/")
.font(.system(size: 18, weight: .medium, design: .monospaced))
.font(.bitchatSystem(size: 18, weight: .medium, design: .monospaced))
.foregroundColor(textColor)
.onTapGesture(count: 3) {
// PANIC: Triple-tap to clear all data
@@ -1095,12 +1101,12 @@ struct ContentView: View {
HStack(spacing: 0) {
Text("@")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
TextField("nickname", text: $viewModel.nickname)
.textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.frame(maxWidth: 80)
.foregroundColor(textColor)
.focused($isNicknameFieldFocused)
@@ -1139,7 +1145,7 @@ struct ContentView: View {
if viewModel.hasAnyUnreadMessages {
Button(action: { viewModel.openMostRelevantPrivateChat() }) {
Image(systemName: "envelope.fill")
.font(.system(size: 12))
.font(.bitchatSystem(size: 12))
.foregroundColor(Color.orange)
}
.buttonStyle(.plain)
@@ -1159,7 +1165,7 @@ struct ContentView: View {
let currentCount = (notesCounter.count ?? 0)
let hasNotes = (!notesCounter.initialLoadComplete ? max(currentCount, sheetNotesCount) : currentCount) > 0
Image(systemName: "long.text.page.and.pencil")
.font(.system(size: 12))
.font(.bitchatSystem(size: 12))
.foregroundColor(hasNotes ? textColor : Color.gray)
.padding(.top, 1)
}
@@ -1173,7 +1179,7 @@ struct ContentView: View {
if case .location(let ch) = locationManager.selectedChannel {
Button(action: { GeohashBookmarksStore.shared.toggle(ch.geohash) }) {
Image(systemName: GeohashBookmarksStore.shared.isBookmarked(ch.geohash) ? "bookmark.fill" : "bookmark")
.font(.system(size: 12))
.font(.bitchatSystem(size: 12))
}
.buttonStyle(.plain)
.accessibilityLabel("Toggle bookmark for #\(ch.geohash)")
@@ -1196,9 +1202,9 @@ struct ContentView: View {
}
}()
Text(badgeText)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(badgeColor)
.lineLimit(1)
.lineLimit(headerLineLimit)
.fixedSize(horizontal: true, vertical: false)
.layoutPriority(2)
.accessibilityLabel("location channels")
@@ -1210,15 +1216,15 @@ struct ContentView: View {
HStack(spacing: 4) {
// People icon with count
Image(systemName: "person.2.fill")
.font(.system(size: 11))
.font(.bitchatSystem(size: 11))
.accessibilityLabel("\(headerOtherPeersCount) people")
Text("\(headerOtherPeersCount)")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.accessibilityHidden(true)
}
.foregroundColor(headerCountColor)
.padding(.leading, 2)
.lineLimit(1)
.lineLimit(headerLineLimit)
.fixedSize(horizontal: true, vertical: false)
// QR moved to the PEOPLE header in the sidebar when on mesh channel
@@ -1235,7 +1241,7 @@ struct ContentView: View {
.environmentObject(viewModel)
}
}
.frame(height: 44)
.frame(height: headerHeight)
.padding(.horizontal, 12)
.sheet(isPresented: $showLocationChannelsSheet) {
LocationChannelsSheet(isPresented: $showLocationChannelsSheet)
@@ -1251,22 +1257,22 @@ struct ContentView: View {
VStack(spacing: 12) {
HStack {
Text("notes")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
Spacer()
Button(action: { showLocationNotes = false }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
.accessibilityLabel("Close")
}
.frame(height: 44)
.frame(height: headerHeight)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
Text("location unavailable")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
Button("enable location") {
LocationChannelManager.shared.enableLocationChannels()
@@ -1417,19 +1423,19 @@ struct ContentView: View {
case .bluetoothConnected:
// Radio icon for mesh connection
Image(systemName: "dot.radiowaves.left.and.right")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.accessibilityLabel("Connected via mesh")
case .meshReachable:
// point.3 filled icon for reachable via mesh (not directly connected)
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.accessibilityLabel("Reachable via mesh")
case .nostrAvailable:
// Purple globe for Nostr
Image(systemName: "globe")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(.purple)
.accessibilityLabel("Available via Nostr")
case .offline:
@@ -1439,25 +1445,25 @@ struct ContentView: View {
} else if viewModel.meshService.isPeerReachable(headerPeerID) {
// Fallback: reachable via mesh but not in current peer list
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.accessibilityLabel("Reachable via mesh")
} else if isNostrAvailable {
// Fallback to Nostr if peer not in list but is mutual favorite
Image(systemName: "globe")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(.purple)
.accessibilityLabel("Available via Nostr")
} else if viewModel.meshService.isPeerConnected(headerPeerID) || viewModel.connectedPeers.contains(headerPeerID) {
// Fallback: if peer lookup is missing but mesh reports connected, show radio
Image(systemName: "dot.radiowaves.left.and.right")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.accessibilityLabel("Connected via mesh")
}
Text("\(privatePeerNick)")
.font(.system(size: 16, weight: .medium, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(textColor) // Dynamic encryption status icon (hide for geohash DMs)
if !privatePeerID.hasPrefix("nostr_") {
// Use short peer ID if available for encryption status (sessions keyed by short ID)
@@ -1470,7 +1476,7 @@ struct ContentView: View {
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
if let icon = encryptionStatus.icon {
Image(systemName: icon)
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
.foregroundColor(encryptionStatus == .noiseVerified ? textColor :
encryptionStatus == .noiseSecured ? textColor :
Color.red)
@@ -1492,7 +1498,7 @@ struct ContentView: View {
}
}) {
Image(systemName: "chevron.left")
.font(.system(size: 12))
.font(.bitchatSystem(size: 12))
.foregroundColor(textColor)
.frame(width: 44, height: 44, alignment: .leading)
.contentShape(Rectangle())
@@ -1508,7 +1514,7 @@ struct ContentView: View {
viewModel.toggleFavorite(peerID: headerPeerID)
}) {
Image(systemName: viewModel.isFavorite(peerID: headerPeerID) ? "star.fill" : "star")
.font(.system(size: 16))
.font(.bitchatSystem(size: 16))
.foregroundColor(viewModel.isFavorite(peerID: headerPeerID) ? Color.yellow : textColor)
}
.buttonStyle(.plain)
@@ -1517,7 +1523,7 @@ struct ContentView: View {
}
}
}
.frame(height: 44)
.frame(height: headerHeight)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
}
@@ -1574,7 +1580,7 @@ private struct PaymentChipView: View {
HStack(spacing: 6) {
Text(emoji)
Text(label)
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
@@ -1615,20 +1621,20 @@ struct DeliveryStatusView: View {
switch status {
case .sending:
Image(systemName: "circle")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(secondaryTextColor.opacity(0.6))
case .sent:
Image(systemName: "checkmark")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(secondaryTextColor.opacity(0.6))
case .delivered(let nickname, _):
HStack(spacing: -2) {
Image(systemName: "checkmark")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
Image(systemName: "checkmark")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
}
.foregroundColor(textColor.opacity(0.8))
.help("Delivered to \(nickname)")
@@ -1636,25 +1642,25 @@ struct DeliveryStatusView: View {
case .read(let nickname, _):
HStack(spacing: -2) {
Image(systemName: "checkmark")
.font(.system(size: 10, weight: .bold))
.font(.bitchatSystem(size: 10, weight: .bold))
Image(systemName: "checkmark")
.font(.system(size: 10, weight: .bold))
.font(.bitchatSystem(size: 10, weight: .bold))
}
.foregroundColor(Color(red: 0.0, green: 0.478, blue: 1.0)) // Bright blue
.help("Read by \(nickname)")
case .failed(let reason):
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(Color.red.opacity(0.8))
.help("Failed: \(reason)")
case .partiallyDelivered(let reached, let total):
HStack(spacing: 1) {
Image(systemName: "checkmark")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
Text("\(reached)/\(total)")
.font(.system(size: 10, design: .monospaced))
.font(.bitchatSystem(size: 10, design: .monospaced))
}
.foregroundColor(secondaryTextColor.opacity(0.6))
.help("Delivered to \(reached) of \(total) members")
+14 -14
View File
@@ -27,14 +27,14 @@ struct FingerprintView: View {
// Header
HStack {
Text("SECURITY VERIFICATION")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.foregroundColor(textColor)
Spacer()
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.system(size: 14, weight: .semibold))
.font(.bitchatSystem(size: 14, weight: .semibold))
}
.foregroundColor(textColor)
}
@@ -66,17 +66,17 @@ struct FingerprintView: View {
HStack {
if let icon = encryptionStatus.icon {
Image(systemName: icon)
.font(.system(size: 20))
.font(.bitchatSystem(size: 20))
.foregroundColor(encryptionStatus == .noiseVerified ? Color.green : textColor)
}
VStack(alignment: .leading, spacing: 4) {
Text(peerNickname)
.font(.system(size: 18, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 18, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
Text(encryptionStatus.description)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
}
@@ -89,12 +89,12 @@ struct FingerprintView: View {
// Their fingerprint
VStack(alignment: .leading, spacing: 8) {
Text("THEIR FINGERPRINT:")
.font(.system(size: 12, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
if let fingerprint = viewModel.getFingerprint(for: statusPeerID) {
Text(formatFingerprint(fingerprint))
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(textColor)
.multilineTextAlignment(.leading)
.lineLimit(nil)
@@ -115,7 +115,7 @@ struct FingerprintView: View {
}
} else {
Text("not available - handshake in progress")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(Color.orange)
.padding()
}
@@ -124,12 +124,12 @@ struct FingerprintView: View {
// My fingerprint
VStack(alignment: .leading, spacing: 8) {
Text("YOUR FINGERPRINT:")
.font(.system(size: 12, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
let myFingerprint = viewModel.getMyFingerprint()
Text(formatFingerprint(myFingerprint))
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(textColor)
.multilineTextAlignment(.leading)
.lineLimit(nil)
@@ -156,14 +156,14 @@ struct FingerprintView: View {
VStack(spacing: 12) {
Text(isVerified ? "✓ VERIFIED" : "⚠️ NOT VERIFIED")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(isVerified ? Color.green : Color.orange)
.frame(maxWidth: .infinity)
Text(isVerified ?
"you have verified this person's identity." :
"compare these fingerprints with \(peerNickname) using a secure channel.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(textColor.opacity(0.7))
.multilineTextAlignment(.center)
.lineLimit(nil)
@@ -176,7 +176,7 @@ struct FingerprintView: View {
dismiss()
}) {
Text("MARK AS VERIFIED")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
@@ -190,7 +190,7 @@ struct FingerprintView: View {
dismiss()
}) {
Text("REMOVE VERIFICATION")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
+6 -6
View File
@@ -12,7 +12,7 @@ struct GeohashPeopleList: View {
if viewModel.visibleGeohashPeople().isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text("nobody around...")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
.padding(.top, 12)
@@ -51,30 +51,30 @@ struct GeohashPeopleList: View {
let icon = teleported ? "face.dashed" : "mappin.and.ellipse"
let assignedColor = viewModel.colorForNostrPubkey(person.id, isDark: colorScheme == .dark)
let rowColor: Color = isMe ? .orange : assignedColor
Image(systemName: icon).font(.system(size: 12)).foregroundColor(rowColor)
Image(systemName: icon).font(.bitchatSystem(size: 12)).foregroundColor(rowColor)
let (base, suffix) = splitSuffix(from: person.displayName)
HStack(spacing: 0) {
Text(base)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.fontWeight(isMe ? .bold : .regular)
.foregroundColor(rowColor)
if !suffix.isEmpty {
let suffixColor = isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
Text(suffix)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(suffixColor)
}
if isMe {
Text(" (you)")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(rowColor)
}
}
if let me = myHex, person.id != me {
if viewModel.isGeohashUserBlocked(pubkeyHexLowercased: person.id) {
Image(systemName: "nosign")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(.red)
.help("Blocked in geochash")
}
+23 -23
View File
@@ -22,12 +22,12 @@ struct LocationChannelsSheet: View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Text("#location channels")
.font(.system(size: 18, design: .monospaced))
.font(.bitchatSystem(size: 18, design: .monospaced))
Spacer()
closeButton
}
Text("chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. your IP address is hidden by routing all traffic over tor.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
Group {
@@ -35,7 +35,7 @@ struct LocationChannelsSheet: View {
case LocationChannelManager.PermissionState.notDetermined:
Button(action: { manager.enableLocationChannels() }) {
Text("get location and my geohashes")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(standardGreen)
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
@@ -46,7 +46,7 @@ struct LocationChannelsSheet: View {
case LocationChannelManager.PermissionState.denied, LocationChannelManager.PermissionState.restricted:
VStack(alignment: .leading, spacing: 8) {
Text("location permission denied. enable in settings to use location channels.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
Button("open settings") { openSystemLocationSettings() }
.buttonStyle(.plain)
@@ -99,7 +99,7 @@ struct LocationChannelsSheet: View {
private var closeButton: some View {
Button(action: { isPresented = false }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -132,7 +132,7 @@ struct LocationChannelsSheet: View {
trailingAccessory: {
Button(action: { bookmarks.toggle(channel.geohash) }) {
Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
}
.buttonStyle(.plain)
.padding(.leading, 8)
@@ -149,7 +149,7 @@ struct LocationChannelsSheet: View {
HStack {
ProgressView()
Text("finding nearby channels…")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
}
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0))
}
@@ -158,7 +158,7 @@ struct LocationChannelsSheet: View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 2) {
Text("#")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(.secondary)
TextField("geohash", text: $customGeohash)
#if os(iOS)
@@ -166,7 +166,7 @@ struct LocationChannelsSheet: View {
.autocorrectionDisabled(true)
.keyboardType(.asciiCapable)
#endif
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.onChange(of: customGeohash) { newValue in
// Allow only geohash base32 characters, strip '#', limit length
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
@@ -194,13 +194,13 @@ struct LocationChannelsSheet: View {
}) {
HStack(spacing: 6) {
Text("teleport")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
Image(systemName: "face.dashed")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
}
}
.buttonStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.padding(.vertical, 6)
.background(Color.secondary.opacity(0.12))
.cornerRadius(6)
@@ -209,7 +209,7 @@ struct LocationChannelsSheet: View {
}
if let err = customError {
Text(err)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.red)
}
}
@@ -219,7 +219,7 @@ struct LocationChannelsSheet: View {
if !bookmarks.bookmarks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("bookmarked")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
VStack(spacing: 6) {
ForEach(bookmarks.bookmarks, id: \.self) { gh in
@@ -236,7 +236,7 @@ struct LocationChannelsSheet: View {
trailingAccessory: {
Button(action: { bookmarks.toggle(gh) }) {
Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark")
.font(.system(size: 14))
.font(.bitchatSystem(size: 14))
}
.buttonStyle(.plain)
.padding(.leading, 8)
@@ -271,7 +271,7 @@ struct LocationChannelsSheet: View {
openSystemLocationSettings()
}) {
Text("remove location access")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1))
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
@@ -319,12 +319,12 @@ struct LocationChannelsSheet: View {
let parts = splitTitleAndCount(title)
HStack(spacing: 4) {
Text(parts.base)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.fontWeight(titleBold ? .bold : .regular)
.foregroundColor(titleColor ?? Color.primary)
if let count = parts.countSuffix, !count.isEmpty {
Text(count)
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(.secondary)
}
}
@@ -335,7 +335,7 @@ struct LocationChannelsSheet: View {
return subtitlePrefix
}()
Text(subtitleFull)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
@@ -343,7 +343,7 @@ struct LocationChannelsSheet: View {
Spacer()
if isSelected {
Text("✔︎")
.font(.system(size: 16, design: .monospaced))
.font(.bitchatSystem(size: 16, design: .monospaced))
.foregroundColor(standardGreen)
}
trailingAccessory()
@@ -425,10 +425,10 @@ extension LocationChannelsSheet {
Toggle(isOn: torToggleBinding) {
VStack(alignment: .leading, spacing: 2) {
Text("tor routing")
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.foregroundColor(.primary)
Text("hides your ip for location channels. recommended: on.")
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(.secondary)
}
}
@@ -459,7 +459,7 @@ private struct IRCToggleStyle: ToggleStyle {
Spacer()
Text(configuration.isOn ? "on" : "off")
.textCase(.uppercase)
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.foregroundColor(configuration.isOn ? accent : .secondary)
.padding(.vertical, 4)
.padding(.horizontal, 10)
+24 -22
View File
@@ -7,6 +7,7 @@ struct LocationNotesView: View {
let onNotesCountChanged: ((Int) -> Void)?
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@ObservedObject private var locationManager = LocationChannelManager.shared
@Environment(\.dismiss) private var dismiss
@State private var draft: String = ""
@@ -20,6 +21,7 @@ struct LocationNotesView: View {
private var backgroundColor: Color { colorScheme == .dark ? .black : .white }
private var accentGreen: Color { colorScheme == .dark ? .green : Color(red: 0, green: 0.5, blue: 0) }
private var maxDraftLines: Int { dynamicTypeSize.isAccessibilitySize ? 5 : 3 }
var body: some View {
#if os(macOS)
@@ -79,7 +81,7 @@ struct LocationNotesView: View {
private var closeButton: some View {
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -91,30 +93,30 @@ struct LocationNotesView: View {
return VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) {
Text("#\(geohash)\(count) \(count == 1 ? "note" : "notes")")
.font(.system(size: 18, design: .monospaced))
.font(.bitchatSystem(size: 18, design: .monospaced))
Spacer()
closeButton
}
if let building = locationManager.locationNames[.building], !building.isEmpty {
Text(building)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(accentGreen)
} else if let block = locationManager.locationNames[.block], !block.isEmpty {
Text(block)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(accentGreen)
}
Text("add short permanent notes to this location for other visitors to find.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if manager.state == .loading && !manager.initialLoadComplete {
Text("loading recent notes…")
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(.secondary)
} else if manager.state == .noRelays {
Text("geo relays unavailable; notes paused")
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(.secondary)
}
}
@@ -152,16 +154,16 @@ struct LocationNotesView: View {
return VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text("@\(baseName)")
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
if !ts.isEmpty {
Text(ts)
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(.secondary)
}
Spacer()
}
Text(note.content)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 4)
@@ -170,12 +172,12 @@ struct LocationNotesView: View {
private var noRelaysRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("no geo relays nearby")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
Text("notes rely on geo relays. check connection and try again.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
Button("retry") { manager.refresh() }
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.buttonStyle(.plain)
}
.padding(.vertical, 6)
@@ -185,7 +187,7 @@ struct LocationNotesView: View {
HStack(spacing: 10) {
ProgressView()
Text("loading notes…")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
Spacer()
}
@@ -195,9 +197,9 @@ struct LocationNotesView: View {
private var emptyRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text("no notes yet")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
Text("be the first to add one for this spot.")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.secondary)
}
.padding(.vertical, 6)
@@ -207,13 +209,13 @@ struct LocationNotesView: View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
Text(message)
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
Spacer()
}
Button("dismiss") { manager.clearError() }
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.buttonStyle(.plain)
}
.padding(.vertical, 6)
@@ -223,12 +225,12 @@ struct LocationNotesView: View {
HStack(alignment: .top, spacing: 10) {
TextField("add a note for this place", text: $draft, axis: .vertical)
.textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.lineLimit(3, reservesSpace: true)
.font(.bitchatSystem(size: 14, design: .monospaced))
.lineLimit(maxDraftLines, reservesSpace: true)
.padding(.vertical, 6)
Button(action: send) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 20))
.font(.bitchatSystem(size: 20))
.foregroundColor(sendButtonEnabled ? accentGreen : .secondary)
}
.padding(.top, 2)
+14 -14
View File
@@ -15,7 +15,7 @@ struct MeshPeerList: View {
if viewModel.allPeers.isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text("nobody around...")
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.padding(.horizontal)
.padding(.top, 12)
@@ -45,27 +45,27 @@ struct MeshPeerList: View {
let baseColor = isMe ? Color.orange : assigned
if isMe {
Image(systemName: "person.fill")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isConnected {
// Mesh-connected peer: radio icon
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isReachable {
// Mesh-reachable (relayed): point.3 icon
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if peer.isMutualFavorite {
// Mutual favorite reachable via Nostr: globe icon (purple)
Image(systemName: "globe")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(.purple)
} else {
// Fallback icon for others (dimmed)
Image(systemName: "person")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(secondaryTextColor)
}
@@ -73,19 +73,19 @@ struct MeshPeerList: View {
let (base, suffix) = splitSuffix(from: displayName)
HStack(spacing: 0) {
Text(base)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(baseColor)
if !suffix.isEmpty {
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
Text(suffix)
.font(.system(size: 14, design: .monospaced))
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(suffixColor)
}
}
if !isMe, viewModel.isPeerBlocked(peer.id) {
Image(systemName: "nosign")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(.red)
.help("Blocked")
}
@@ -94,7 +94,7 @@ struct MeshPeerList: View {
if peer.isConnected {
if let icon = item.enc.icon {
Image(systemName: icon)
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
}
} else {
@@ -102,12 +102,12 @@ struct MeshPeerList: View {
if let fp = viewModel.getFingerprint(for: peer.id),
viewModel.verifiedFingerprints.contains(fp) {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
} else if let icon = item.enc.icon {
// Fallback to whatever status says (likely lock if we had a past session)
Image(systemName: icon)
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(baseColor)
}
}
@@ -118,7 +118,7 @@ struct MeshPeerList: View {
// Unread message indicator for this peer
if !isMe, item.hasUnread {
Image(systemName: "envelope.fill")
.font(.system(size: 10))
.font(.bitchatSystem(size: 10))
.foregroundColor(.orange)
.help("New messages")
}
@@ -126,7 +126,7 @@ struct MeshPeerList: View {
if !isMe {
Button(action: { onToggleFavorite(peer.id) }) {
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
.font(.system(size: 12))
.font(.bitchatSystem(size: 12))
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
}
.buttonStyle(.plain)
+10 -10
View File
@@ -16,7 +16,7 @@ struct MyQRView: View {
var body: some View {
VStack(spacing: 12) {
Text("scan to verify me")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
VStack(spacing: 10) {
QRCodeImage(data: qrString, size: 240)
@@ -24,7 +24,7 @@ struct MyQRView: View {
// Non-scrolling, fully visible URL (wraps across lines)
Text(qrString)
.font(.system(size: 11, design: .monospaced))
.font(.bitchatSystem(size: 11, design: .monospaced))
.textSelection(.enabled)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
@@ -60,7 +60,7 @@ struct QRCodeImage: View {
.frame(width: size, height: size)
.overlay(
Text("qr unavailable")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(.gray)
)
}
@@ -119,7 +119,7 @@ struct QRScanView: View {
.clipShape(RoundedRectangle(cornerRadius: 8))
#else
Text("paste qr content to validate:")
.font(.system(size: 14, weight: .medium, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .medium, design: .monospaced))
TextEditor(text: $input)
.frame(height: 100)
.border(Color.gray.opacity(0.4))
@@ -255,7 +255,7 @@ struct VerificationSheetView: View {
// Top header (always at top)
HStack {
Text("VERIFY")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(accentColor)
Spacer()
Button(action: {
@@ -263,7 +263,7 @@ struct VerificationSheetView: View {
isPresented = false
}) {
Image(systemName: "xmark")
.font(.system(size: 14, weight: .semibold))
.font(.bitchatSystem(size: 14, weight: .semibold))
.foregroundColor(accentColor)
}
.buttonStyle(.plain)
@@ -279,7 +279,7 @@ struct VerificationSheetView: View {
if showingScanner {
VStack(alignment: .leading, spacing: 12) {
Text("scan a friend's qr")
.font(.system(size: 16, weight: .bold, design: .monospaced))
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.foregroundColor(accentColor)
@@ -310,13 +310,13 @@ struct VerificationSheetView: View {
if showingScanner {
Button(action: { showingScanner = false }) {
Label("show my qr", systemImage: "qrcode")
.font(.system(size: 13, design: .monospaced))
.font(.bitchatSystem(size: 13, design: .monospaced))
}
.buttonStyle(.bordered)
} else {
Button(action: { showingScanner = true }) {
Label("scan someone else's qr", systemImage: "camera.viewfinder")
.font(.system(size: 13, weight: .medium, design: .monospaced))
.font(.bitchatSystem(size: 13, weight: .medium, design: .monospaced))
}
.buttonStyle(.bordered)
.tint(.gray)
@@ -328,7 +328,7 @@ struct VerificationSheetView: View {
viewModel.verifiedFingerprints.contains(fp) {
Button(action: { viewModel.unverifyFingerprint(for: pid) }) {
Label("remove verification", systemImage: "minus.circle")
.font(.system(size: 12, design: .monospaced))
.font(.bitchatSystem(size: 12, design: .monospaced))
}
.buttonStyle(.bordered)
.tint(.gray)
+16
View File
@@ -0,0 +1,16 @@
import SwiftUI
import XCTest
@testable import bitchat
final class FontBitchatTests: XCTestCase {
func testMonospacedMapping() {
XCTAssertEqual(Font.bitchatSystem(size: 10, design: .monospaced), Font.system(.caption2, design: .monospaced))
XCTAssertEqual(Font.bitchatSystem(size: 14, design: .monospaced), Font.system(.body, design: .monospaced))
XCTAssertEqual(Font.bitchatSystem(size: 20, design: .monospaced), Font.system(.title2, design: .monospaced))
}
func testWeightIsPreserved() {
let bold = Font.bitchatSystem(size: 14, weight: .bold, design: .monospaced)
XCTAssertEqual(bold, Font.system(.body, design: .monospaced).weight(.bold))
}
}