import SwiftUI struct LocationNotesView: View { @EnvironmentObject var viewModel: ChatViewModel @StateObject private var manager: LocationNotesManager let geohash: String @Environment(\.colorScheme) var colorScheme @State private var draft: String = "" init(geohash: String) { let gh = geohash.lowercased() self.geohash = gh _manager = StateObject(wrappedValue: LocationNotesManager(geohash: gh)) } private var backgroundColor: Color { colorScheme == .dark ? Color.black : Color.white } private var textColor: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) } private var secondaryTextColor: Color { colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8) } var body: some View { ZStack { VStack(spacing: 0) { header Divider() list Divider() input } if manager.isLoading { MatrixRainView() .transition(.opacity) } } .background(backgroundColor) .foregroundColor(textColor) .onDisappear { manager.cancel() } } private var header: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text("notes @ #\(geohash)") .font(.system(size: 16, weight: .bold, design: .monospaced)) Text("street-level notes") .font(.system(size: 12, design: .monospaced)) .foregroundColor(secondaryTextColor) } Spacer() } .frame(height: 44) .padding(.horizontal, 12) .background(backgroundColor.opacity(0.95)) } private var list: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 8) { ForEach(manager.notes) { note in VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(note.displayName) .font(.system(size: 12, weight: .semibold, design: .monospaced)) .foregroundColor(secondaryTextColor) Text(note.createdAt, style: .relative) .font(.system(size: 11, design: .monospaced)) .foregroundColor(secondaryTextColor.opacity(0.8)) } Text(note.content) .font(.system(size: 14, design: .monospaced)) .fixedSize(horizontal: false, vertical: true) } .padding(.horizontal, 12) } } .padding(.vertical, 8) } .background(backgroundColor) } private var input: some View { HStack(alignment: .center, spacing: 8) { TextField("add a note for this place", text: $draft, axis: .vertical) .textFieldStyle(.plain) .font(.system(size: 14, design: .monospaced)) .lineLimit(3, reservesSpace: true) .padding(.horizontal, 12) Button(action: send) { Image(systemName: "arrow.up.circle.fill") .font(.system(size: 20)) .foregroundColor(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Color.gray : textColor) } .buttonStyle(.plain) .disabled(draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) .padding(.trailing, 12) } .frame(minHeight: 44) .padding(.vertical, 8) .background(backgroundColor.opacity(0.95)) } private func send() { let content = draft.trimmingCharacters(in: .whitespacesAndNewlines) guard !content.isEmpty else { return } manager.send(content: content, nickname: viewModel.nickname) draft = "" } } // MARK: - Matrix Rain Loader private struct MatrixRainView: View { @Environment(\.colorScheme) var colorScheme private var fg: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) } private let charset = Array("01abcdefghijklmnopqrstuvwxyzアイウエオカキクケコハヒフヘホ0123456789") var body: some View { GeometryReader { geo in let w = max(geo.size.width, 1) let h = max(geo.size.height, 1) let colWidth: CGFloat = 14 let cols = max(Int(w / colWidth), 6) ZStack { ForEach(0.. Double { let p = Double(idx) / Double(max(glyphs.count, 1)) return 0.15 + (1.0 - p) * 0.85 } private func randomGlyphs(_ n: Int) -> [String] { (0..