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