From de906cb97cc3f94c7b525118b6c5e59002747072 Mon Sep 17 00:00:00 2001 From: jack Date: Wed, 24 Sep 2025 12:31:45 +0200 Subject: [PATCH] Refresh typography and channel sheet styling --- Configs/Release.xcconfig | 2 +- bitchat/Utils/Font+Bitchat.swift | 14 +- bitchat/Views/ContentView.swift | 6 +- bitchat/Views/LocationChannelsSheet.swift | 348 ++++++++++++---------- 4 files changed, 199 insertions(+), 171 deletions(-) diff --git a/Configs/Release.xcconfig b/Configs/Release.xcconfig index e6a84775..03d9d1f4 100644 --- a/Configs/Release.xcconfig +++ b/Configs/Release.xcconfig @@ -1,4 +1,4 @@ -MARKETING_VERSION = 1.4.2 +MARKETING_VERSION = 1.4.3 CURRENT_PROJECT_VERSION = 1 IPHONEOS_DEPLOYMENT_TARGET = 16.0 diff --git a/bitchat/Utils/Font+Bitchat.swift b/bitchat/Utils/Font+Bitchat.swift index 968a4af9..f51be2ea 100644 --- a/bitchat/Utils/Font+Bitchat.swift +++ b/bitchat/Utils/Font+Bitchat.swift @@ -20,15 +20,19 @@ private extension Font.TextStyle { return .caption2 case ..<13.0: return .caption - case ..<14.0: + case ..<13.75: return .footnote - case ..<16.5: + case ..<15.5: + return .subheadline + case ..<17.5: + return .callout + case ..<19.5: return .body - case ..<19.0: + case ..<22.5: return .title3 - case ..<23.0: + case ..<27.5: return .title2 - case ..<30.0: + case ..<34.0: return .title default: return .largeTitle diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 926f2a2a..4d6623e3 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -55,6 +55,8 @@ struct ContentView: View { @State private var notesGeohash: String? = nil @State private var sheetNotesCount: Int = 0 @ScaledMetric(relativeTo: .body) private var headerHeight: CGFloat = 44 + @ScaledMetric(relativeTo: .subheadline) private var headerPeerIconSize: CGFloat = 11 + @ScaledMetric(relativeTo: .subheadline) private var headerPeerCountFontSize: CGFloat = 12 // Timer-based refresh removed; use LocationChannelManager live updates instead // Window sizes for rendering (infinite scroll up) @State private var windowCountPublic: Int = 300 @@ -1216,10 +1218,10 @@ struct ContentView: View { HStack(spacing: 4) { // People icon with count Image(systemName: "person.2.fill") - .font(.bitchatSystem(size: 11)) + .font(.system(size: headerPeerIconSize, weight: .regular)) .accessibilityLabel("\(headerOtherPeersCount) people") Text("\(headerOtherPeersCount)") - .font(.bitchatSystem(size: 12, design: .monospaced)) + .font(.system(size: headerPeerCountFontSize, weight: .regular, design: .monospaced)) .accessibilityHidden(true) } .foregroundColor(headerCountColor) diff --git a/bitchat/Views/LocationChannelsSheet.swift b/bitchat/Views/LocationChannelsSheet.swift index ac6e39d1..8fac888f 100644 --- a/bitchat/Views/LocationChannelsSheet.swift +++ b/bitchat/Views/LocationChannelsSheet.swift @@ -107,188 +107,212 @@ struct LocationChannelsSheet: View { } private var channelList: some View { - List { - // Mesh option first (no bookmark) - channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) { - manager.select(ChannelID.mesh) - isPresented = false - } - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) + ScrollView { + LazyVStack(spacing: 0) { + channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) { + manager.select(ChannelID.mesh) + isPresented = false + } + .padding(.vertical, 6) - // Nearby options - if !manager.availableChannels.isEmpty { - ForEach(manager.availableChannels.filter { $0.level != .building }) { channel in - let coverage = coverageString(forPrecision: channel.geohash.count) - let nameBase = locationName(for: channel.level) - let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 } - let subtitlePrefix = "#\(channel.geohash) • \(coverage)" - let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0 + let nearby = manager.availableChannels.filter { $0.level != .building } + if !nearby.isEmpty { + ForEach(nearby) { channel in + sectionDivider + let coverage = coverageString(forPrecision: channel.geohash.count) + let nameBase = locationName(for: channel.level) + let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 } + let subtitlePrefix = "#\(channel.geohash) • \(coverage)" + let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 0 + channelRow( + title: geohashTitleWithCount(for: channel), + subtitlePrefix: subtitlePrefix, + subtitleName: namePart, + isSelected: isSelected(channel), + titleBold: highlight, + trailingAccessory: { + Button(action: { bookmarks.toggle(channel.geohash) }) { + Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark") + .font(.bitchatSystem(size: 14)) + } + .buttonStyle(.plain) + .padding(.leading, 8) + } + ) { + manager.markTeleported(for: channel.geohash, false) + manager.select(ChannelID.location(channel)) + isPresented = false + } + .padding(.vertical, 6) + } + } else { + sectionDivider + HStack(spacing: 8) { + ProgressView() + Text("finding nearby channels…") + .font(.bitchatSystem(size: 12, design: .monospaced)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 10) + } + + sectionDivider + customTeleportSection + .padding(.vertical, 8) + + let bookmarkedList = bookmarks.bookmarks + if !bookmarkedList.isEmpty { + sectionDivider + bookmarkedSection(bookmarkedList) + .padding(.vertical, 8) + } + + if manager.permissionState == LocationChannelManager.PermissionState.authorized { + sectionDivider + torToggleSection + .padding(.top, 12) + Button(action: { + openSystemLocationSettings() + }) { + Text("remove location access") + .font(.bitchatSystem(size: 12, design: .monospaced)) + .foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1)) + .frame(maxWidth: .infinity) + .padding(.vertical, 6) + .background(Color.red.opacity(0.08)) + .cornerRadius(6) + } + .buttonStyle(.plain) + .padding(.vertical, 8) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 6) + .background(backgroundColor) + } + .background(backgroundColor) + } + + private var sectionDivider: some View { + Rectangle() + .fill(dividerColor) + .frame(height: 1) + } + + private var dividerColor: Color { + colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08) + } + + private var customTeleportSection: some View { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 2) { + Text("#") + .font(.bitchatSystem(size: 14, design: .monospaced)) + .foregroundColor(.secondary) + TextField("geohash", text: $customGeohash) + #if os(iOS) + .textInputAutocapitalization(.never) + .autocorrectionDisabled(true) + .keyboardType(.asciiCapable) + #endif + .font(.bitchatSystem(size: 14, design: .monospaced)) + .onChange(of: customGeohash) { newValue in + let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz") + let filtered = newValue + .lowercased() + .replacingOccurrences(of: "#", with: "") + .filter { allowed.contains($0) } + if filtered.count > 12 { + customGeohash = String(filtered.prefix(12)) + } else if filtered != newValue { + customGeohash = filtered + } + } + let normalized = customGeohash + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + .replacingOccurrences(of: "#", with: "") + let isValid = validateGeohash(normalized) + Button(action: { + let gh = normalized + guard isValid else { customError = "invalid geohash"; return } + let level = levelForLength(gh.count) + let ch = GeohashChannel(level: level, geohash: gh) + manager.markTeleported(for: ch.geohash, true) + manager.select(ChannelID.location(ch)) + isPresented = false + }) { + HStack(spacing: 6) { + Text("teleport") + .font(.bitchatSystem(size: 14, design: .monospaced)) + Image(systemName: "face.dashed") + .font(.bitchatSystem(size: 14)) + } + } + .buttonStyle(.plain) + .font(.bitchatSystem(size: 14, design: .monospaced)) + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.secondary.opacity(0.12)) + .cornerRadius(6) + .opacity(isValid ? 1.0 : 0.4) + .disabled(!isValid) + } + if let err = customError { + Text(err) + .font(.bitchatSystem(size: 12, design: .monospaced)) + .foregroundColor(.red) + } + } + } + + private func bookmarkedSection(_ entries: [String]) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text("bookmarked") + .font(.bitchatSystem(size: 12, design: .monospaced)) + .foregroundColor(.secondary) + LazyVStack(spacing: 0) { + ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in + let level = levelForLength(gh.count) + let channel = GeohashChannel(level: level, geohash: gh) + let coverage = coverageString(forPrecision: gh.count) + let subtitle = "#\(gh) • \(coverage)" + let name = bookmarks.bookmarkNames[gh] channelRow( - title: geohashTitleWithCount(for: channel), - subtitlePrefix: subtitlePrefix, - subtitleName: namePart, + title: geohashHashTitleWithCount(gh), + subtitlePrefix: subtitle, + subtitleName: name.map { formattedNamePrefix(for: level) + $0 }, isSelected: isSelected(channel), - titleBold: highlight, trailingAccessory: { - Button(action: { bookmarks.toggle(channel.geohash) }) { - Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark") + Button(action: { bookmarks.toggle(gh) }) { + Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark") .font(.bitchatSystem(size: 14)) } .buttonStyle(.plain) .padding(.leading, 8) } ) { - // Selecting a suggested nearby channel is not a teleport. Persist this. - manager.markTeleported(for: channel.geohash, false) + let inRegional = manager.availableChannels.contains { $0.geohash == gh } + if !inRegional && !manager.availableChannels.isEmpty { + manager.markTeleported(for: gh, true) + } else { + manager.markTeleported(for: gh, false) + } manager.select(ChannelID.location(channel)) isPresented = false } - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) - } - } else { - HStack { - ProgressView() - Text("finding nearby channels…") - .font(.bitchatSystem(size: 12, design: .monospaced)) - } - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) - } - - // Custom geohash teleport - VStack(alignment: .leading, spacing: 6) { - HStack(spacing: 2) { - Text("#") - .font(.bitchatSystem(size: 14, design: .monospaced)) - .foregroundColor(.secondary) - TextField("geohash", text: $customGeohash) - #if os(iOS) - .textInputAutocapitalization(.never) - .autocorrectionDisabled(true) - .keyboardType(.asciiCapable) - #endif - .font(.bitchatSystem(size: 14, design: .monospaced)) - .onChange(of: customGeohash) { newValue in - // Allow only geohash base32 characters, strip '#', limit length - let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz") - let filtered = newValue - .lowercased() - .replacingOccurrences(of: "#", with: "") - .filter { allowed.contains($0) } - if filtered.count > 12 { - customGeohash = String(filtered.prefix(12)) - } else if filtered != newValue { - customGeohash = filtered - } - } - let normalized = customGeohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased().replacingOccurrences(of: "#", with: "") - let isValid = validateGeohash(normalized) - Button(action: { - let gh = normalized - guard isValid else { customError = "invalid geohash"; return } - let level = levelForLength(gh.count) - let ch = GeohashChannel(level: level, geohash: gh) - // Mark this selection as a manual teleport - manager.markTeleported(for: ch.geohash, true) - manager.select(ChannelID.location(ch)) - isPresented = false - }) { - HStack(spacing: 6) { - Text("teleport") - .font(.bitchatSystem(size: 14, design: .monospaced)) - Image(systemName: "face.dashed") - .font(.bitchatSystem(size: 14)) - } - } - .buttonStyle(.plain) - .font(.bitchatSystem(size: 14, design: .monospaced)) .padding(.vertical, 6) - .background(Color.secondary.opacity(0.12)) - .cornerRadius(6) - .opacity(isValid ? 1.0 : 0.4) - .disabled(!isValid) - } - if let err = customError { - Text(err) - .font(.bitchatSystem(size: 12, design: .monospaced)) - .foregroundColor(.red) - } - } - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) + .onAppear { bookmarks.resolveNameIfNeeded(for: gh) } - // Bookmarked geohashes - if !bookmarks.bookmarks.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("bookmarked") - .font(.bitchatSystem(size: 12, design: .monospaced)) - .foregroundColor(.secondary) - VStack(spacing: 6) { - ForEach(bookmarks.bookmarks, id: \.self) { gh in - let level = levelForLength(gh.count) - let channel = GeohashChannel(level: level, geohash: gh) - let coverage = coverageString(forPrecision: gh.count) - let subtitle = "#\(gh) • \(coverage)" - let name = bookmarks.bookmarkNames[gh] - channelRow( - title: geohashHashTitleWithCount(gh), - subtitlePrefix: subtitle, - subtitleName: name.map { formattedNamePrefix(for: level) + $0 }, - isSelected: isSelected(channel), - trailingAccessory: { - Button(action: { bookmarks.toggle(gh) }) { - Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark") - .font(.bitchatSystem(size: 14)) - } - .buttonStyle(.plain) - .padding(.leading, 8) - } - ) { - // For bookmarked selection, mark teleported based on regional membership - let inRegional = manager.availableChannels.contains { $0.geohash == gh } - if !inRegional && !manager.availableChannels.isEmpty { - manager.markTeleported(for: gh, true) - } else { - manager.markTeleported(for: gh, false) - } - manager.select(ChannelID.location(channel)) - isPresented = false - } - .onAppear { bookmarks.resolveNameIfNeeded(for: gh) } - } + if index < entries.count - 1 { + sectionDivider } - .padding(12) - .background(Color.secondary.opacity(0.12)) - .cornerRadius(8) } - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) - } - - // Footer action inside the list - if manager.permissionState == LocationChannelManager.PermissionState.authorized { - torToggleSection - .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 4, trailing: 0)) - Button(action: { - openSystemLocationSettings() - }) { - Text("remove location access") - .font(.bitchatSystem(size: 12, design: .monospaced)) - .foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1)) - .frame(maxWidth: .infinity) - .padding(.vertical, 6) - .background(Color.red.opacity(0.08)) - .cornerRadius(6) - } - .buttonStyle(.plain) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 10, trailing: 0)) } } - .listStyle(.plain) - .scrollContentBackground(.hidden) - .background(backgroundColor) } + private func isSelected(_ channel: GeohashChannel) -> Bool { if case .location(let ch) = manager.selectedChannel { return ch == channel @@ -437,8 +461,6 @@ extension LocationChannelsSheet { .padding(12) .background(Color.secondary.opacity(0.12)) .cornerRadius(8) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) } private var standardGreen: Color {