Refresh typography and channel sheet styling

This commit is contained in:
jack
2025-09-24 12:32:11 +02:00
parent a41ec65f58
commit de906cb97c
4 changed files with 199 additions and 171 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
MARKETING_VERSION = 1.4.2 MARKETING_VERSION = 1.4.3
CURRENT_PROJECT_VERSION = 1 CURRENT_PROJECT_VERSION = 1
IPHONEOS_DEPLOYMENT_TARGET = 16.0 IPHONEOS_DEPLOYMENT_TARGET = 16.0
+9 -5
View File
@@ -20,15 +20,19 @@ private extension Font.TextStyle {
return .caption2 return .caption2
case ..<13.0: case ..<13.0:
return .caption return .caption
case ..<14.0: case ..<13.75:
return .footnote return .footnote
case ..<16.5: case ..<15.5:
return .subheadline
case ..<17.5:
return .callout
case ..<19.5:
return .body return .body
case ..<19.0: case ..<22.5:
return .title3 return .title3
case ..<23.0: case ..<27.5:
return .title2 return .title2
case ..<30.0: case ..<34.0:
return .title return .title
default: default:
return .largeTitle return .largeTitle
+4 -2
View File
@@ -55,6 +55,8 @@ struct ContentView: View {
@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 @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 // 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
@@ -1216,10 +1218,10 @@ 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(.bitchatSystem(size: 11)) .font(.system(size: headerPeerIconSize, weight: .regular))
.accessibilityLabel("\(headerOtherPeersCount) people") .accessibilityLabel("\(headerOtherPeersCount) people")
Text("\(headerOtherPeersCount)") Text("\(headerOtherPeersCount)")
.font(.bitchatSystem(size: 12, design: .monospaced)) .font(.system(size: headerPeerCountFontSize, weight: .regular, design: .monospaced))
.accessibilityHidden(true) .accessibilityHidden(true)
} }
.foregroundColor(headerCountColor) .foregroundColor(headerCountColor)
+185 -163
View File
@@ -107,188 +107,212 @@ struct LocationChannelsSheet: View {
} }
private var channelList: some View { private var channelList: some View {
List { ScrollView {
// Mesh option first (no bookmark) LazyVStack(spacing: 0) {
channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) { channelRow(title: meshTitleWithCount(), subtitlePrefix: "#bluetooth • \(bluetoothRangeString())", isSelected: isMeshSelected, titleColor: standardBlue, titleBold: meshCount() > 0) {
manager.select(ChannelID.mesh) manager.select(ChannelID.mesh)
isPresented = false isPresented = false
} }
.listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) .padding(.vertical, 6)
// Nearby options let nearby = manager.availableChannels.filter { $0.level != .building }
if !manager.availableChannels.isEmpty { if !nearby.isEmpty {
ForEach(manager.availableChannels.filter { $0.level != .building }) { channel in ForEach(nearby) { channel in
let coverage = coverageString(forPrecision: channel.geohash.count) sectionDivider
let nameBase = locationName(for: channel.level) let coverage = coverageString(forPrecision: channel.geohash.count)
let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 } let nameBase = locationName(for: channel.level)
let subtitlePrefix = "#\(channel.geohash)\(coverage)" let namePart = nameBase.map { formattedNamePrefix(for: channel.level) + $0 }
let highlight = viewModel.geohashParticipantCount(for: channel.geohash) > 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( channelRow(
title: geohashTitleWithCount(for: channel), title: geohashHashTitleWithCount(gh),
subtitlePrefix: subtitlePrefix, subtitlePrefix: subtitle,
subtitleName: namePart, subtitleName: name.map { formattedNamePrefix(for: level) + $0 },
isSelected: isSelected(channel), isSelected: isSelected(channel),
titleBold: highlight,
trailingAccessory: { trailingAccessory: {
Button(action: { bookmarks.toggle(channel.geohash) }) { Button(action: { bookmarks.toggle(gh) }) {
Image(systemName: bookmarks.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark") Image(systemName: bookmarks.isBookmarked(gh) ? "bookmark.fill" : "bookmark")
.font(.bitchatSystem(size: 14)) .font(.bitchatSystem(size: 14))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.padding(.leading, 8) .padding(.leading, 8)
} }
) { ) {
// Selecting a suggested nearby channel is not a teleport. Persist this. let inRegional = manager.availableChannels.contains { $0.geohash == gh }
manager.markTeleported(for: channel.geohash, false) if !inRegional && !manager.availableChannels.isEmpty {
manager.markTeleported(for: gh, true)
} else {
manager.markTeleported(for: gh, false)
}
manager.select(ChannelID.location(channel)) manager.select(ChannelID.location(channel))
isPresented = false 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) .padding(.vertical, 6)
.background(Color.secondary.opacity(0.12)) .onAppear { bookmarks.resolveNameIfNeeded(for: gh) }
.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))
// Bookmarked geohashes if index < entries.count - 1 {
if !bookmarks.bookmarks.isEmpty { sectionDivider
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) }
}
} }
.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 { private func isSelected(_ channel: GeohashChannel) -> Bool {
if case .location(let ch) = manager.selectedChannel { if case .location(let ch) = manager.selectedChannel {
return ch == channel return ch == channel
@@ -437,8 +461,6 @@ extension LocationChannelsSheet {
.padding(12) .padding(12)
.background(Color.secondary.opacity(0.12)) .background(Color.secondary.opacity(0.12))
.cornerRadius(8) .cornerRadius(8)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
} }
private var standardGreen: Color { private var standardGreen: Color {