From c49a1b264eeef466fa93f67971d23e0dd211c7ba Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Tue, 23 Sep 2025 19:56:16 +0200 Subject: [PATCH] Support Dynamic Type across chat surfaces (#664) Co-authored-by: jack --- bitchat/Utils/Font+Bitchat.swift | 37 ++++++++ bitchat/ViewModels/ChatViewModel.swift | 52 +++++------ bitchat/Views/AppInfoView.swift | 30 ++++--- bitchat/Views/ContentView.swift | 104 ++++++++++++---------- bitchat/Views/FingerprintView.swift | 28 +++--- bitchat/Views/GeohashPeopleList.swift | 12 +-- bitchat/Views/LocationChannelsSheet.swift | 46 +++++----- bitchat/Views/LocationNotesView.swift | 46 +++++----- bitchat/Views/MeshPeerList.swift | 28 +++--- bitchat/Views/VerificationViews.swift | 20 ++--- bitchatTests/FontBitchatTests.swift | 16 ++++ 11 files changed, 245 insertions(+), 174 deletions(-) create mode 100644 bitchat/Utils/Font+Bitchat.swift create mode 100644 bitchatTests/FontBitchatTests.swift diff --git a/bitchat/Utils/Font+Bitchat.swift b/bitchat/Utils/Font+Bitchat.swift new file mode 100644 index 00000000..968a4af9 --- /dev/null +++ b/bitchat/Utils/Font+Bitchat.swift @@ -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 + } + } +} diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 6d1fdc93..732a2d91 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -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.. 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") diff --git a/bitchat/Views/FingerprintView.swift b/bitchat/Views/FingerprintView.swift index a3d2541a..4d600dff 100644 --- a/bitchat/Views/FingerprintView.swift +++ b/bitchat/Views/FingerprintView.swift @@ -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) diff --git a/bitchat/Views/GeohashPeopleList.swift b/bitchat/Views/GeohashPeopleList.swift index f646733f..cfd9f1fa 100644 --- a/bitchat/Views/GeohashPeopleList.swift +++ b/bitchat/Views/GeohashPeopleList.swift @@ -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") } diff --git a/bitchat/Views/LocationChannelsSheet.swift b/bitchat/Views/LocationChannelsSheet.swift index 1d21279a..ac6e39d1 100644 --- a/bitchat/Views/LocationChannelsSheet.swift +++ b/bitchat/Views/LocationChannelsSheet.swift @@ -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) diff --git a/bitchat/Views/LocationNotesView.swift b/bitchat/Views/LocationNotesView.swift index 5313615c..1c42ae5d 100644 --- a/bitchat/Views/LocationNotesView.swift +++ b/bitchat/Views/LocationNotesView.swift @@ -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) diff --git a/bitchat/Views/MeshPeerList.swift b/bitchat/Views/MeshPeerList.swift index 8e03b8ec..2ba20a7c 100644 --- a/bitchat/Views/MeshPeerList.swift +++ b/bitchat/Views/MeshPeerList.swift @@ -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) diff --git a/bitchat/Views/VerificationViews.swift b/bitchat/Views/VerificationViews.swift index b5fa3a42..275d0946 100644 --- a/bitchat/Views/VerificationViews.swift +++ b/bitchat/Views/VerificationViews.swift @@ -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) diff --git a/bitchatTests/FontBitchatTests.swift b/bitchatTests/FontBitchatTests.swift new file mode 100644 index 00000000..37df8f19 --- /dev/null +++ b/bitchatTests/FontBitchatTests.swift @@ -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)) + } +}