mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:05:19 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
429c88aa1e | ||
|
|
6f3b61c3df | ||
|
|
3f1a9686dd | ||
|
|
179378031a | ||
|
|
03b70e2c2f | ||
|
|
08782f0196 | ||
|
|
e855a2e40b | ||
|
|
2b567bfb97 | ||
|
|
a6d98de90e | ||
|
|
ea22ccda71 | ||
|
|
19533742ff | ||
|
|
e80f725906 | ||
|
|
c7d8c95f8d | ||
|
|
743551c10c | ||
|
|
7da6f95e87 | ||
|
|
bd753af6cb | ||
|
|
a3f54c5bf8 | ||
|
|
79a045af39 | ||
|
|
607acdbfe4 | ||
|
|
5b3f1a1350 | ||
|
|
ee4ce4a38e |
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Lightweight background counter for location notes (kind 1) at block-level geohash.
|
/// Lightweight background counter for location notes (kind 1) at building-level geohash (8 chars).
|
||||||
@MainActor
|
@MainActor
|
||||||
final class LocationNotesCounter: ObservableObject {
|
final class LocationNotesCounter: ObservableObject {
|
||||||
static let shared = LocationNotesCounter()
|
static let shared = LocationNotesCounter()
|
||||||
@@ -16,10 +16,11 @@ final class LocationNotesCounter: ObservableObject {
|
|||||||
|
|
||||||
func subscribe(geohash gh: String) {
|
func subscribe(geohash gh: String) {
|
||||||
let norm = gh.lowercased()
|
let norm = gh.lowercased()
|
||||||
if geohash == norm { return }
|
if geohash == norm, subscriptionID != nil { return }
|
||||||
cancel()
|
// Unsubscribe previous without clearing count to avoid flicker
|
||||||
|
if let sub = subscriptionID { NostrRelayManager.shared.unsubscribe(id: sub) }
|
||||||
|
subscriptionID = nil
|
||||||
geohash = norm
|
geohash = norm
|
||||||
count = 0
|
|
||||||
noteIDs.removeAll()
|
noteIDs.removeAll()
|
||||||
initialLoadComplete = false
|
initialLoadComplete = false
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendPrivateMessageNotification(from sender: String, message: String, peerID: String) {
|
func sendPrivateMessageNotification(from sender: String, message: String, peerID: String) {
|
||||||
let title = "🔒 private message from \(sender)"
|
let title = "🔒 DM from \(sender)"
|
||||||
let body = message
|
let body = message
|
||||||
let identifier = "private-\(UUID().uuidString)"
|
let identifier = "private-\(UUID().uuidString)"
|
||||||
let userInfo = ["peerID": peerID, "senderName": sender]
|
let userInfo = ["peerID": peerID, "senderName": sender]
|
||||||
|
|||||||
@@ -86,10 +86,14 @@ struct AppInfoView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("close") {
|
Button(action: { dismiss() }) {
|
||||||
dismiss()
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
}
|
}
|
||||||
.foregroundColor(textColor)
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel("Close")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1140,7 +1140,31 @@ struct ContentView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel("Open unread private chat")
|
.accessibilityLabel("Open unread private chat")
|
||||||
}
|
}
|
||||||
// Bookmark toggle for current geohash (not shown for mesh)
|
// Notes icon (mesh only and when location is authorized), to the left of #mesh
|
||||||
|
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
|
||||||
|
Button(action: {
|
||||||
|
// Kick a one-shot refresh and show the sheet immediately.
|
||||||
|
LocationChannelManager.shared.enableLocationChannels()
|
||||||
|
LocationChannelManager.shared.refreshChannels()
|
||||||
|
// If we already have a block geohash, pass it; otherwise wait in the sheet.
|
||||||
|
notesGeohash = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||||
|
showLocationNotes = true
|
||||||
|
}) {
|
||||||
|
HStack(alignment: .center, spacing: 4) {
|
||||||
|
let currentCount = (notesCounter.count ?? 0)
|
||||||
|
let hasNotes = (!notesCounter.initialLoadComplete ? max(currentCount, sheetNotesCount) : currentCount) > 0
|
||||||
|
Image(systemName: "long.text.page.and.pencil")
|
||||||
|
.font(.system(size: 12))
|
||||||
|
.foregroundColor(hasNotes ? textColor : Color.gray)
|
||||||
|
.padding(.top, 1)
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel("Location notes for this place")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bookmark toggle (geochats): to the left of #geohash
|
||||||
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")
|
||||||
@@ -1149,6 +1173,7 @@ struct ContentView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel("Toggle bookmark for #\(ch.geohash)")
|
.accessibilityLabel("Toggle bookmark for #\(ch.geohash)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Location channels button '#'
|
// Location channels button '#'
|
||||||
Button(action: { showLocationChannelsSheet = true }) {
|
Button(action: { showLocationChannelsSheet = true }) {
|
||||||
let badgeText: String = {
|
let badgeText: String = {
|
||||||
@@ -1174,29 +1199,8 @@ struct ContentView: View {
|
|||||||
.accessibilityLabel("location channels")
|
.accessibilityLabel("location channels")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.padding(.leading, 4)
|
||||||
// Notes icon (mesh only and when location is authorized), to the right of #mesh
|
.padding(.trailing, 2)
|
||||||
if case .mesh = locationManager.selectedChannel, locationManager.permissionState == .authorized {
|
|
||||||
Button(action: {
|
|
||||||
// Kick a one-shot refresh and show the sheet immediately.
|
|
||||||
LocationChannelManager.shared.enableLocationChannels()
|
|
||||||
LocationChannelManager.shared.refreshChannels()
|
|
||||||
// If we already have a block geohash, pass it; otherwise wait in the sheet.
|
|
||||||
notesGeohash = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
|
||||||
showLocationNotes = true
|
|
||||||
}) {
|
|
||||||
HStack(alignment: .center, spacing: 4) {
|
|
||||||
let hasNotes = ((notesCounter.count ?? 0) > 0) || (sheetNotesCount > 0)
|
|
||||||
Image(systemName: "long.text.page.and.pencil")
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.foregroundColor(hasNotes ? Color(hue: 0.60, saturation: 0.85, brightness: 0.82) : Color.gray)
|
|
||||||
.padding(.top, 1)
|
|
||||||
}
|
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityLabel("Location notes for this place")
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
// People icon with count
|
// People icon with count
|
||||||
@@ -1208,6 +1212,7 @@ struct ContentView: View {
|
|||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
.foregroundColor(headerCountColor)
|
.foregroundColor(headerCountColor)
|
||||||
|
.padding(.leading, 2)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
|
|
||||||
@@ -1277,11 +1282,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
LocationChannelManager.shared.endLiveRefresh()
|
LocationChannelManager.shared.endLiveRefresh()
|
||||||
sheetNotesCount = 0
|
|
||||||
}
|
}
|
||||||
.onChange(of: locationManager.availableChannels) { channels in
|
.onChange(of: locationManager.availableChannels) { channels in
|
||||||
if let current = channels.first(where: { $0.level == .building })?.geohash,
|
if let current = channels.first(where: { $0.level == .building })?.geohash,
|
||||||
notesGeohash != current {
|
notesGeohash != current {
|
||||||
notesGeohash = current
|
notesGeohash = current
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// Light taptic when geohash changes while the sheet is open
|
// Light taptic when geohash changes while the sheet is open
|
||||||
@@ -1524,8 +1528,16 @@ extension ContentView {
|
|||||||
if locationManager.permissionState == .authorized {
|
if locationManager.permissionState == .authorized {
|
||||||
LocationChannelManager.shared.refreshChannels()
|
LocationChannelManager.shared.refreshChannels()
|
||||||
}
|
}
|
||||||
if let building = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
|
if locationManager.permissionState == .authorized {
|
||||||
LocationNotesCounter.shared.subscribe(geohash: building)
|
if let building = LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash {
|
||||||
|
LocationNotesCounter.shared.subscribe(geohash: building)
|
||||||
|
} else {
|
||||||
|
// Keep existing subscription if we had one to avoid flicker
|
||||||
|
// Only cancel if we have no known geohash
|
||||||
|
if LocationNotesCounter.shared.geohash == nil {
|
||||||
|
LocationNotesCounter.shared.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LocationNotesCounter.shared.cancel()
|
LocationNotesCounter.shared.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ struct LocationChannelsSheet: View {
|
|||||||
@State private var customGeohash: String = ""
|
@State private var customGeohash: String = ""
|
||||||
@State private var customError: String? = nil
|
@State private var customError: String? = nil
|
||||||
|
|
||||||
|
private var backgroundColor: Color { colorScheme == .dark ? .black : .white }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("#location channels")
|
Text("#location channels")
|
||||||
.font(.system(size: 18, design: .monospaced))
|
.font(.system(size: 18, design: .monospaced))
|
||||||
Text("chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps.")
|
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(.system(size: 12, design: .monospaced))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
@@ -54,19 +56,30 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
|
.background(backgroundColor)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("close") { isPresented = false }
|
Button(action: { isPresented = false }) {
|
||||||
.font(.system(size: 14, design: .monospaced))
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel("Close")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .automatic) {
|
ToolbarItem(placement: .automatic) {
|
||||||
Button("close") { isPresented = false }
|
Button(action: { isPresented = false }) {
|
||||||
.font(.system(size: 14, design: .monospaced))
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel("Close")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -77,6 +90,7 @@ struct LocationChannelsSheet: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 420, minHeight: 520)
|
.frame(minWidth: 420, minHeight: 520)
|
||||||
#endif
|
#endif
|
||||||
|
.background(backgroundColor)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Refresh channels when opening
|
// Refresh channels when opening
|
||||||
if manager.permissionState == LocationChannelManager.PermissionState.authorized {
|
if manager.permissionState == LocationChannelManager.PermissionState.authorized {
|
||||||
@@ -205,44 +219,49 @@ struct LocationChannelsSheet: View {
|
|||||||
|
|
||||||
// Bookmarked geohashes
|
// Bookmarked geohashes
|
||||||
if !bookmarks.bookmarks.isEmpty {
|
if !bookmarks.bookmarks.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("bookmarked")
|
Text("bookmarked")
|
||||||
.font(.system(size: 12, design: .monospaced))
|
.font(.system(size: 12, design: .monospaced))
|
||||||
.foregroundColor(.secondary)
|
.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(.system(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)
|
.listRowSeparator(.hidden)
|
||||||
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(.system(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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer action inside the list
|
// Footer action inside the list
|
||||||
@@ -260,9 +279,12 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(backgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isSelected(_ channel: GeohashChannel) -> Bool {
|
private func isSelected(_ channel: GeohashChannel) -> Bool {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ struct LocationNotesSheet: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if let gh = notesGeohash ?? locationManager.availableChannels.first(where: { $0.level == .block })?.geohash {
|
if let gh = notesGeohash ?? locationManager.availableChannels.first(where: { $0.level == .building })?.geohash {
|
||||||
// Found block geohash: show notes view
|
// Found block geohash: show notes view
|
||||||
LocationNotesView(geohash: gh)
|
LocationNotesView(geohash: gh)
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
@@ -55,8 +55,8 @@ struct LocationNotesSheet: View {
|
|||||||
LocationChannelManager.shared.refreshChannels()
|
LocationChannelManager.shared.refreshChannels()
|
||||||
}
|
}
|
||||||
.onChange(of: locationManager.availableChannels) { channels in
|
.onChange(of: locationManager.availableChannels) { channels in
|
||||||
if notesGeohash == nil, let block = channels.first(where: { $0.level == .block }) {
|
if notesGeohash == nil, let building = channels.first(where: { $0.level == .building }) {
|
||||||
notesGeohash = block.geohash
|
notesGeohash = building.geohash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ struct LocationNotesView: View {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
// Slightly darker green for hash suffix emphasis
|
||||||
|
private var darkerTextColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.4, blue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -55,8 +59,11 @@ struct LocationNotesView: View {
|
|||||||
let c = manager.notes.count
|
let c = manager.notes.count
|
||||||
Text("\(c) \(c == 1 ? "note" : "notes") ")
|
Text("\(c) \(c == 1 ? "note" : "notes") ")
|
||||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
Text("@ #\(geohash)")
|
Text("@ ")
|
||||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
|
Text("#\(geohash)")
|
||||||
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
}
|
}
|
||||||
if let buildingName = locationManager.locationNames[.building], !buildingName.isEmpty {
|
if let buildingName = locationManager.locationNames[.building], !buildingName.isEmpty {
|
||||||
Text(buildingName)
|
Text(buildingName)
|
||||||
@@ -90,10 +97,18 @@ struct LocationNotesView: View {
|
|||||||
ForEach(manager.notes) { note in
|
ForEach(manager.notes) { note in
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(note.displayName)
|
// Show @name without the #abcd suffix; timestamp in brackets
|
||||||
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
HStack(spacing: 0) {
|
||||||
.foregroundColor(secondaryTextColor)
|
Text("@")
|
||||||
Text(timestampText(for: note.createdAt))
|
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
let parts = splitSuffix(from: note.displayName)
|
||||||
|
Text(parts.0)
|
||||||
|
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
let ts = timestampText(for: note.createdAt)
|
||||||
|
Text(ts.isEmpty ? "" : "[\(ts)]")
|
||||||
.font(.system(size: 11, design: .monospaced))
|
.font(.system(size: 11, design: .monospaced))
|
||||||
.foregroundColor(secondaryTextColor.opacity(0.8))
|
.foregroundColor(secondaryTextColor.opacity(0.8))
|
||||||
}
|
}
|
||||||
@@ -174,3 +189,16 @@ struct LocationNotesView: View {
|
|||||||
return f
|
return f
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to split a trailing #abcd suffix
|
||||||
|
private func splitSuffix(from name: String) -> (String, String) {
|
||||||
|
guard name.count >= 5 else { return (name, "") }
|
||||||
|
let suffix = String(name.suffix(5))
|
||||||
|
if suffix.first == "#", suffix.dropFirst().allSatisfy({ c in
|
||||||
|
("0"..."9").contains(String(c)) || ("a"..."f").contains(String(c)) || ("A"..."F").contains(String(c))
|
||||||
|
}) {
|
||||||
|
let base = String(name.dropLast(5))
|
||||||
|
return (base, suffix)
|
||||||
|
}
|
||||||
|
return (name, "")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user